Skip to content

feat: Add Guppy type aliases#1645

Open
ss2165 wants to merge 1 commit into
mainfrom
ss/push-qxzxrlrrysko
Open

feat: Add Guppy type aliases#1645
ss2165 wants to merge 1 commit into
mainfrom
ss/push-qxzxrlrrysko

Conversation

@ss2165
Copy link
Copy Markdown
Member

@ss2165 ss2165 commented Apr 7, 2026

This PR adds first-class Guppy type aliases via guppy.type_alias(...).

It introduces:

  • alias definitions in the type-definition pipeline
  • alias resolution during type parsing and instantiation

Recursive and mutually recursive aliases are rejected.

The recursion check follows the same general strategy already used for recursive structs/enums:

  • temporarily intercept alias instantiation while parsing the alias body
  • detect recursive re-entry
  • raise a compiler error

For aliases, this now produces alias-specific diagnostics with:

  • a dedicated RecursiveTypeAliasError
  • notes pointing at the alias definitions involved in the cycle
  • a help hint suggesting how to break it

AI use: authored via locally iterating with OpenAI Codex, basing implementation on prior work in structs.

Closes #1066

@ss2165 ss2165 requested a review from mark-koch April 7, 2026 16:51
@ss2165 ss2165 requested a review from a team as a code owner April 7, 2026 16:51
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

🐰 Bencher Report

Branchss/push-qxzxrlrrysko
TestbedLinux
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
tests/benchmarks/test_big_array.py::test_big_array_check📈 view plot
🚷 view threshold
748,145.64 µs
(-22.86%)Baseline: 969,889.72 µs
1,018,384.21 µs
(73.46%)
tests/benchmarks/test_big_array.py::test_big_array_compile📈 view plot
🚷 view threshold
1,983,094.49 µs
(-6.51%)Baseline: 2,121,140.19 µs
2,227,197.19 µs
(89.04%)
tests/benchmarks/test_big_array.py::test_big_array_executable📈 view plot
🚷 view threshold
8,717,920.21 µs
(-1.47%)Baseline: 8,848,222.71 µs
9,290,633.84 µs
(93.84%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_check📈 view plot
🚷 view threshold
106,705.05 µs
(-5.55%)Baseline: 112,979.47 µs
118,628.45 µs
(89.95%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_compile📈 view plot
🚷 view threshold
233,179.75 µs
(-3.01%)Baseline: 240,419.53 µs
252,440.51 µs
(92.37%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_executable📈 view plot
🚷 view threshold
974,931.97 µs
(-28.33%)Baseline: 1,360,286.89 µs
1,428,301.23 µs
(68.26%)
tests/benchmarks/test_prelude.py::test_import_guppy📈 view plot
🚷 view threshold
49.75 µs
(-5.59%)Baseline: 52.70 µs
55.33 µs
(89.91%)
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

🐰 Bencher Report

Branchss/push-qxzxrlrrysko
TestbedLinux
Click to view all benchmark results
Benchmarkhugr_bytesBenchmark Result
bytes x 1e3
(Result Δ%)
Upper Boundary
bytes x 1e3
(Limit %)
hugr_nodesBenchmark Result
nodes x 1e3
(Result Δ%)
Upper Boundary
nodes x 1e3
(Limit %)
tests/benchmarks/test_big_array.py::test_big_array_compile📈 view plot
🚷 view threshold
158.77 x 1e3
(0.00%)Baseline: 158.77 x 1e3
160.36 x 1e3
(99.01%)
📈 view plot
🚷 view threshold
6.64 x 1e3
(0.00%)Baseline: 6.64 x 1e3
6.71 x 1e3
(99.01%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_compile📈 view plot
🚷 view threshold
27.53 x 1e3
(0.00%)Baseline: 27.53 x 1e3
27.81 x 1e3
(99.01%)
📈 view plot
🚷 view threshold
1.07 x 1e3
(0.00%)Baseline: 1.07 x 1e3
1.08 x 1e3
(99.01%)
🐰 View full continuous benchmarking report in Bencher

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 99.05660% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.59%. Comparing base (931c12b) to head (4678592).

Files with missing lines Patch % Lines
...ernals/src/guppylang_internals/definition/alias.py 99.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1645      +/-   ##
==========================================
+ Coverage   93.54%   93.59%   +0.04%     
==========================================
  Files         133      134       +1     
  Lines       12640    12746     +106     
==========================================
+ Hits        11824    11929     +105     
- Misses        816      817       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ss2165 ss2165 force-pushed the ss/push-qxzxrlrrysko branch from 71b3742 to fa6be65 Compare April 7, 2026 16:55
Base automatically changed from ss/push-ywqnpopsywvw to main April 7, 2026 17:51
@maximilianruesch maximilianruesch changed the title feat: add Guppy type aliases feat: Add Guppy type aliases Apr 8, 2026
@ss2165 ss2165 force-pushed the ss/push-qxzxrlrrysko branch from fa6be65 to 3ec6f5c Compare April 9, 2026 13:13
This PR adds first-class Guppy type aliases via `guppy.type_alias(...)`.

It introduces:
- alias definitions in the type-definition pipeline
- alias resolution during type parsing and instantiation

Recursive and mutually recursive aliases are rejected.

The recursion check follows the same general strategy already used for recursive structs/enums:
- temporarily intercept alias instantiation while parsing the alias body
- detect recursive re-entry instead of recursing forever
- raise a compiler error

For aliases, this now produces alias-specific diagnostics with:
- a dedicated `RecursiveTypeAliasError`
- notes pointing at the alias definitions involved in the cycle
- a help hint suggesting how to break it

Authored with OpenAI Codex.

Closes #1066
@ss2165 ss2165 force-pushed the ss/push-qxzxrlrrysko branch from 3ec6f5c to 4678592 Compare April 13, 2026 13:44


def test_type_alias_bad_type_syntax():
with pytest.raises(SyntaxError, match="Not a valid Guppy type: `foo bar`"):
Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an error tests for invalid aliases that fail at a later point, i.e. via a GuppyError instead of a SyntaxError?

Comment on lines +17 to +18
Help: Type aliases must eventually resolve to a non-alias type. Break the cycle
by inlining one alias or introducing a struct or enum wrapper.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlining can never break cycles? Similarly, I don't see how structs or enums would help?

Comment on lines +8 to +12
Note:
|
3 |
4 | MyAlias = guppy.type_alias("MyAlias")
| ------------------------------------- Alias `MyAlias` is part of this cycle
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note doesn't seem useful if it's a self cycle

)
defn = RawTypeAliasDef(
DefId.fresh(),
ty,
Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets the name of the definition as the type string which seems odd.

I see that you use a separate _alias_name function to resolve the name later by inspecting the Python scope. Note that this is quite different from how we handle naming of definition in other parts of the compiler. Usually, guppy.type_var etc take the definition name as an argument.

Checking the scope seems reasonable though, but it should happen here in the decorator! Also, the fallback probably shouldn't be the type string. Maybe, instead raise an error and force users to pass it as an argument?

Comment on lines +95 to +97
ctx = TypeParsingCtx(globals, allow_free_vars=True)
ty = type_from_ast(self.type_ast, ctx)
params = tuple(ctx.param_var_mapping.values())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an attempt to support generic aliases? I think those would be useful, but we should either add tests for generic aliases or just set params = [] here

Comment on lines +192 to +194
err.add_sub_diagnostic(
RecursiveTypeAliasError.AliasNote(defn.defined_at, alias_name)
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail to render if the aliases are defined in separate files

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While testing, I also found some other rendering errors. For example

Alias1 = guppy.type_alias("Alias2")
Alias2 = guppy.type_alias("Alias3")
Alias3 = guppy.type_alias("Alias2")

@guppy
def main(x: Alias1) -> Alias1:
    return x

main.check()

fails with

Error in sys.excepthook:
Traceback (most recent call last):
  File "guppylang-internals/src/guppylang_internals/error.py", line 120, in hook
    renderer.render_diagnostic(err.error)
  File "guppylang-internals/src/guppylang_internals/diagnostic.py", line 323, in render_diagnostic
    self.render_snippet(
  File "guppylang-internals/src/guppylang_internals/diagnostic.py", line 419, in render_snippet
    leading_whitespace = min(len(line) - len(line.lstrip()) for line in all_lines)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: min() iterable argument is empty

Not sure what's going on there...

# cycle, so avoid attaching the same note more than once.
if any(
isinstance(child, RecursiveTypeAliasError.AliasNote)
and child.alias_name == alias_name
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going by name feels a bit fragile, can we store DefIds instead?

Comment on lines +173 to +174
if isinstance(err.error, RecursiveTypeAliasError):
_add_alias_note(err.error, defn, ctx.globals)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we need to add the notes while unwinding the exception. We already have the cycle, can't we just add all notes in one go?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some tests for how the alias acyclicity checking interacts with structs and enums? I.e. add some integration and error tests that involve struct fields/enum variants whose types contain aliases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type aliases

3 participants