lang: opaque types — zero-cost newtypes - #10
Merged
Merged
Conversation
`opaque type UserId = string` (optionally parameterized): a nominal type distinct from its underlying everywhere, wrapped by the same-named constructor and unwrapped by the single-case pattern, fully ERASED at lowering — no Python class, wrap compiles to the bare value, first-class constructor references compile to _pf_id, and externs typed with the newtype pass the underlying value to Python unchanged. Checker-side a newtype registers as a single-ctor sum sharing the type name, so inference, pattern checking, and Maranget exhaustiveness need no new rules; newtypes export cross-module like sums (with a newtype flag in the project ImportContext so consumers erase too). Erasure made one latent shape reachable: an erased single-case pattern is irrefutable to Python, which rejects any later `case` as a SyntaxError — lowered matches are now sealed by `seal_cases` (truncate at the first unconditional LOWERED case, else append the defensive raise), judged on PyPattern rather than the source arm. `opaque` is a contextual keyword (only before `type`); Scala 3 spelling chosen over Nim's `distinct`/bare `type X = int` (the latter is Python 3.12's transparent-alias syntax — a false friend). Also: ROADMAP notes a pre-existing module-alias shadowing hazard found while testing (`import Ids` + `let ids = ...` clobbers the emitted `import ids`).
simontreanor
added a commit
that referenced
this pull request
Jul 27, 2026
`opaque type` (PR #10) highlighting: tree-sitter rule opaque_type_definition (+ regenerated parser, corpus entry, highlight queries for the bundled/nvim/zed copies), VS Code TextMate keyword, nvim syntax keyword, emacs keyword. Corpus expectation derived from the grammar rules; tree-sitter test could not run on this machine (no C compiler headers) - run it where a compiler exists before the next grammar release.
simontreanor
added a commit
that referenced
this pull request
Jul 27, 2026
* lowering: alias a module import shadowed by a top-level binding `import Ids` + `let ids = ...` emitted `ids = ...` after `import ids`, clobbering the module object so any later qualified call broke at runtime. Qualified-reference emission now routes through `py_module_ref`: normally the plain lowercase module name (readable output unchanged), but when a top-level binding claims that name the import is aliased (`import ids as _pf_ids`) and every reference uses the alias. Local-binder shadowing inside a single function remains and is tracked in ROADMAP as residual. * editors: opaque keyword in grammars `opaque type` (PR #10) highlighting: tree-sitter rule opaque_type_definition (+ regenerated parser, corpus entry, highlight queries for the bundled/nvim/zed copies), VS Code TextMate keyword, nvim syntax keyword, emacs keyword. Corpus expectation derived from the grammar rules; tree-sitter test could not run on this machine (no C compiler headers) - run it where a compiler exists before the next grammar release.
Merged
simontreanor
added a commit
that referenced
this pull request
Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Scala-3-style opaque types adapted to Pyfun:
opaque type UserId = string(optionally parameterized,opaque type Tag a = List a) declares a zero-cost newtype — nominally distinct from its underlying type everywhere, fully erased in the emitted Python. DESIGN.md §7.3 is the canonical write-up.Surface. Wrap with the same-named constructor (
UserId : string -> UserId), unwrap with the single-case pattern (case UserId s:, payload position takes any pattern).opaqueis a contextual keyword — only beforetype;let opaque = 1still parses. Syntax chosen over Nim-styledistinctand baretype X = int(Python 3.12's transparent-alias form — a false friend).Checker. A newtype registers as a single-constructor sum sharing the type name, so inference, pattern checking, and Maranget exhaustiveness (witness
UserId _) need no new rules. Distinctness errors readexpected string, found UserId.Erasure. No class emitted;
UserId "u-1001"→"u-1001";case UserId s:→ barecase s:; first-class references (List.map UserId) →_pf_id; externs typed with a newtype hand Python the bare underlying value (the interop payoff over the single-case-DU idiom, whose wrapper object would leak across the boundary).Match sealing fix. An erased pattern is irrefutable to Python, which rejects any later
caseas a SyntaxError — lowered matches are now sealed byseal_cases: truncate at the first unconditional lowered case, else append the defensive raise. Judged onPyPattern, not the source arm.Cross-module. Newtypes export like sums, plus a newtype flag through the project
ImportContextso consumers eraseIds.UserIdconstruction/patterns/first-class refs identically. Verified by new project tests.Tests. 29 added across roundtrip, typecheck, compile (shape + e2e), project (cross-module e2e + distinctness), and REPL. Full suite green; clippy clean.
Also. ROADMAP now tracks a pre-existing module-alias shadowing hazard found while testing (
import Ids+let ids = …clobbers the emittedimport ids; recommended small fix). Follow-up left for the editor grammars:opaquekeyword highlighting (VS Code/tree-sitter), which rides the manual Marketplace republish.