diff --git a/examples/0151-types-backtick-raw-identifier.sx b/examples/0151-types-backtick-raw-identifier.sx index 186bfbe..5338d22 100644 --- a/examples/0151-types-backtick-raw-identifier.sx +++ b/examples/0151-types-backtick-raw-identifier.sx @@ -1,10 +1,12 @@ // Backtick raw-identifier escape: a leading backtick makes the following -// identifier RAW — its text excludes the backtick and it is NEVER -// type-classified, so a reserved type-name spelling (`s2`, `u8`, …) can be -// used as a value identifier. Exercised in every position: global, local, -// param, struct field + member access, function name + call, and a later -// reference. A *bare* `s2` is still the reserved type name (see -// examples/1119), so the escape is the only way to spell these as values. +// identifier RAW — its text excludes the backtick and it is never the +// reserved/builtin keyword, so a reserved type-name spelling (`s2`, `u8`, …) +// can be used as an ordinary identifier. Exercised in every VALUE position: +// global, local, param, struct field + member access, function name + call, +// and a later reference. (A raw identifier in TYPE position references a +// backtick-declared type instead — see examples/0154.) A *bare* `s2` is still +// the reserved type name (see examples/1119), so the escape is the only way to +// spell these as values. // Regression (issue 0089). #import "modules/std.sx"; diff --git a/examples/0154-types-backtick-raw-type-reference.sx b/examples/0154-types-backtick-raw-type-reference.sx new file mode 100644 index 0000000..9935b45 --- /dev/null +++ b/examples/0154-types-backtick-raw-type-reference.sx @@ -0,0 +1,42 @@ +// Backtick raw identifier in TYPE position (the universal model, issue 0089): +// `` `name `` is the LITERAL identifier `name` used as a type reference, never +// the builtin/reserved spelling. A reserved type spelling (`s2`, `u8`, …) can +// therefore both DECLARE a type (struct / enum / union / error-set / alias) and +// be REFERENCED as that type via the backtick — while a BARE `s2` in type +// position remains the signed-int type (see `add` below) and a bare reserved- +// name declaration still errors (see examples/1141). The backtick is required +// to declare or reference these names; it is never part of the name's text. +// Regression (issue 0089 — attempt-4 universal raw identifier). +#import "modules/std.sx"; + +// Type-introducing decls whose NAME is a reserved spelling. +`s2 :: struct { x: s64; } +`s8 :: enum { A; B; } +`u16 :: union { i: s32; f: f32; } +`u32 :: error { Bad, Empty } +RawAlias :: `s2; // alias to a backtick-declared struct + +// A bare `s2` in type position is still the 2-bit signed int. +add :: (a: s2, b: s2) -> s2 { return a + b; } + +main :: () -> s32 { + // Reference the backtick struct as a type; field access works. + v : `s2 = ---; + v.x = 7; + + // Reference via a normal alias too. + a : RawAlias = ---; + a.x = 11; + + // Backtick enum / union type references. + e : `s8 = .A; + u : `u16 = ---; + u.i = 5; + + print("struct = {}\n", v.x); + print("alias = {}\n", a.x); + print("enum = {}\n", e == .A); + print("union = {}\n", u.i); + print("bare = {}\n", add(1, 0)); // bare s2 = the 2-bit int type + return 0; +} diff --git a/examples/0155-types-backtick-typed-const-union-tag.sx b/examples/0155-types-backtick-typed-const-union-tag.sx new file mode 100644 index 0000000..476e65e --- /dev/null +++ b/examples/0155-types-backtick-typed-const-union-tag.sx @@ -0,0 +1,24 @@ +// Backtick raw identifier at the two remaining binding positions (issue 0089, +// attempt-4): a TYPED constant (`` `s2 : s64 : 5 ``) and a union TAG / field +// (`` `s2: s32 ``). The typed-const form previously slipped past the decl check +// without a name span (caret at 1:1); a bare `s2 : s64 : 5` is still rejected +// with the caret ON the name (see examples/1141). A union tag spelled with a +// reserved name works and is accessible bare or backticked. +// Regression (issue 0089 — attempt-4 typed const + union tag). +#import "modules/std.sx"; + +// Typed constant whose name is a reserved type spelling. +`s2 : s64 : 5; + +// Union whose tags are reserved type spellings. +Mix :: union { `s1: s32; `u8: f32; } + +main :: () -> s32 { + print("typed const = {}\n", `s2); + + m : Mix = ---; + m.`s1 = 42; + print("union tick = {}\n", m.`s1); // backtick member access + print("union bare = {}\n", m.s1); // bare member access — same field + return 0; +} diff --git a/examples/1139-diagnostics-backtick-raw-not-a-type.sx b/examples/1139-diagnostics-backtick-raw-not-a-type.sx deleted file mode 100644 index bfdb943..0000000 --- a/examples/1139-diagnostics-backtick-raw-not-a-type.sx +++ /dev/null @@ -1,12 +0,0 @@ -// A backtick raw identifier is a VALUE-name escape; it is never a type. Using -// one in type position (`x : `s2 = 1`) is a clean parse error, not a silent -// type-classification — reserved type names are the lowercase `sN`/`uN`/`fNN` -// spellings, and a real type never needs a backtick. A *bare* `s2` in type -// position remains the reserved signed-int type. -// Regression (issue 0089 — attempt-2: raw identifier rejected in type position). -#import "modules/std.sx"; - -main :: () -> s32 { - x : `s2 = 1; - return 0; -} diff --git a/examples/1141-diagnostics-reserved-name-type-decl.sx b/examples/1141-diagnostics-reserved-name-type-decl.sx new file mode 100644 index 0000000..d7cc59c --- /dev/null +++ b/examples/1141-diagnostics-reserved-name-type-decl.sx @@ -0,0 +1,22 @@ +// A reserved/builtin type-name spelling is rejected as the NAME of EVERY +// type-introducing `::` declaration too — struct, enum, union, error-set, and +// a typed constant — not just `:=` / value-const / function names (those are +// examples/1140). Each is a declaration-name binding site: a bare reserved +// spelling there mis-classifies and is rejected, exactly like `s2 := …`. The +// backtick escape (`` `s2 :: struct{…} ``, examples/0154) is the only way to +// spell these names in handwritten sx; `#import c` foreign decls stay exempt +// (examples/1220). +// +// Regression (issue 0089 — attempt-4: 0076 holds across every decl kind). +// Expected: one error per declaration, each caret ON the declared name; exit 1. +#import "modules/std.sx"; + +s8 :: struct { v: s64; } +s16 :: enum { A; B; } +u16 :: union { a: s32; b: f32; } +u32 :: error { Bad, Empty } +s2 : s64 : 5; + +main :: () -> s32 { + return 0; +} diff --git a/examples/expected/0154-types-backtick-raw-type-reference.exit b/examples/expected/0154-types-backtick-raw-type-reference.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0154-types-backtick-raw-type-reference.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stdout b/examples/expected/0154-types-backtick-raw-type-reference.stderr similarity index 100% rename from examples/expected/1139-diagnostics-backtick-raw-not-a-type.stdout rename to examples/expected/0154-types-backtick-raw-type-reference.stderr diff --git a/examples/expected/0154-types-backtick-raw-type-reference.stdout b/examples/expected/0154-types-backtick-raw-type-reference.stdout new file mode 100644 index 0000000..7bd00e5 --- /dev/null +++ b/examples/expected/0154-types-backtick-raw-type-reference.stdout @@ -0,0 +1,5 @@ +struct = 7 +alias = 11 +enum = true +union = 5 +bare = 1 diff --git a/examples/expected/0155-types-backtick-typed-const-union-tag.exit b/examples/expected/0155-types-backtick-typed-const-union-tag.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0155-types-backtick-typed-const-union-tag.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0155-types-backtick-typed-const-union-tag.stderr b/examples/expected/0155-types-backtick-typed-const-union-tag.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0155-types-backtick-typed-const-union-tag.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0155-types-backtick-typed-const-union-tag.stdout b/examples/expected/0155-types-backtick-typed-const-union-tag.stdout new file mode 100644 index 0000000..b7e6c01 --- /dev/null +++ b/examples/expected/0155-types-backtick-typed-const-union-tag.stdout @@ -0,0 +1,3 @@ +typed const = 5 +union tick = 42 +union bare = 42 diff --git a/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stderr b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stderr deleted file mode 100644 index 8608dcb..0000000 --- a/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `s2` is a raw identifier, not a type — the backtick escape names a value, never a type - --> examples/1139-diagnostics-backtick-raw-not-a-type.sx:10:10 - | -10 | x : `s2 = 1; - | ^^ diff --git a/examples/expected/1139-diagnostics-backtick-raw-not-a-type.exit b/examples/expected/1141-diagnostics-reserved-name-type-decl.exit similarity index 100% rename from examples/expected/1139-diagnostics-backtick-raw-not-a-type.exit rename to examples/expected/1141-diagnostics-reserved-name-type-decl.exit diff --git a/examples/expected/1141-diagnostics-reserved-name-type-decl.stderr b/examples/expected/1141-diagnostics-reserved-name-type-decl.stderr new file mode 100644 index 0000000..55360ea --- /dev/null +++ b/examples/expected/1141-diagnostics-reserved-name-type-decl.stderr @@ -0,0 +1,29 @@ +error: 's8' is a reserved type name and cannot be used as an identifier + --> examples/1141-diagnostics-reserved-name-type-decl.sx:14:1 + | +14 | s8 :: struct { v: s64; } + | ^^ + +error: 's16' is a reserved type name and cannot be used as an identifier + --> examples/1141-diagnostics-reserved-name-type-decl.sx:15:1 + | +15 | s16 :: enum { A; B; } + | ^^^ + +error: 'u16' is a reserved type name and cannot be used as an identifier + --> examples/1141-diagnostics-reserved-name-type-decl.sx:16:1 + | +16 | u16 :: union { a: s32; b: f32; } + | ^^^ + +error: 'u32' is a reserved type name and cannot be used as an identifier + --> examples/1141-diagnostics-reserved-name-type-decl.sx:17:1 + | +17 | u32 :: error { Bad, Empty } + | ^^^ + +error: 's2' is a reserved type name and cannot be used as an identifier + --> examples/1141-diagnostics-reserved-name-type-decl.sx:18:1 + | +18 | s2 : s64 : 5; + | ^^ diff --git a/examples/expected/1141-diagnostics-reserved-name-type-decl.stdout b/examples/expected/1141-diagnostics-reserved-name-type-decl.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1141-diagnostics-reserved-name-type-decl.stdout @@ -0,0 +1 @@ + diff --git a/issues/0089-backtick-raw-identifier.md b/issues/0089-backtick-raw-identifier.md index c5caafd..8c0df3d 100644 --- a/issues/0089-backtick-raw-identifier.md +++ b/issues/0089-backtick-raw-identifier.md @@ -1,62 +1,68 @@ # 0089 — backtick raw-identifier escape + `#import c` foreign-name exemption from the reserved-type-name rule -> **✅ RESOLVED** (foundation step F0.6). Two mechanisms, per Agra's design ruling: +> **✅ RESOLVED** (foundation step F0.6). Two mechanisms, per Agra's design +> ruling; the final shape is the **universal raw identifier** (attempt 4): +> `` `name `` is THE LITERAL identifier `name`, usable in EVERY position — value, +> declaration, AND type — meaning only "treat this token as a plain identifier, +> never the reserved keyword/type." The backtick is never part of the name's text. > > 1. **Backtick raw identifier.** The lexer recognises a leading backtick > (`` `s2 ``) and emits an `.identifier` token whose span excludes the backtick, -> carrying a `Token.is_raw` flag ([src/lexer.zig], [src/token.zig]). A raw -> identifier is NEVER type-classified — the parser skips `Type.fromName` for it -> in expression position ([src/parser.zig] `parsePrimary`), so it is always a -> value identifier. The `is_raw` flag threads through `ast.Identifier` and EVERY -> binding/capture form ([src/ast.zig]): `VarDecl` / `Param` plus `IfExpr` / -> `WhileExpr` optional bindings, `ForExpr` capture + index, `MatchArm` capture, -> `CatchExpr` / `OnFailStmt` tag bindings, `DestructureDecl` per-name, and the -> protocol-default-body / foreign-class method param lists. `UnknownTypeChecker` -> skips the reserved-name check at each of those arms when raw -> ([src/ir/semantic_diagnostics.zig]). The backtick works in every identifier -> position (local, global, param, field, function name, struct member, later -> reference, and all the control-flow/capture/binding forms). +> carrying a `Token.is_raw` flag ([src/lexer.zig], [src/token.zig]). The flag +> threads through `ast.Identifier`, `ast.TypeExpr`, and EVERY binding / capture / +> declaration node ([src/ast.zig]): `VarDecl` / `ConstDecl` / `Param` / `FnDecl` +> plus `IfExpr` / `WhileExpr` optional bindings, `ForExpr` capture + index, +> `MatchArm` capture, `CatchExpr` / `OnFailStmt` tag bindings, `DestructureDecl` +> per-name, protocol-default / foreign-class method params, AND every +> type-introducing decl — `StructDecl` / `EnumDecl` / `UnionDecl` / +> `ErrorSetDecl` / `ProtocolDecl` / `ForeignClassDecl` / `UfcsAlias` / +> `NamespaceDecl` / `ImportDecl` / `CImportDecl` / `LibraryDecl`. > -> The `::` DECLARATION forms are binding sites too and are equally covered -> (F0.6 attempt-3): a bare reserved-name **constant** (`s2 :: 5`), **function** -> (`s2 :: (…) {…}`, incl. struct/impl methods), or **type** declaration -> (`struct`/`enum`/`union`/`error`/alias/`protocol`/foreign-class/ufcs/namespace) -> is rejected, exactly like `s2 := …`. `ConstDecl`/`FnDecl` carry `is_raw` + -> `name_span` threaded from the parser (`parseConstBinding`/`parseFnDecl`), so the -> backtick form (`` `s2 :: … ``) is exempt; the compiler's own builtin definition -> (`string :: []u8 #builtin`) is the sole non-backtick exemption (a `#builtin` -> constant defines the reserved type). This closed the attempt-2 hole where a -> bare `s2 :: (…) {…}` compiled silently and the call rewrite made it callable. +> - **Value position.** The parser skips `Type.fromName` for a raw identifier +> in expression position ([src/parser.zig] `parsePrimary`), so `` `s2 `` is a +> value identifier; a later bare reference resolves to the binding. +> - **Type position.** `parseTypeExpr` emits a raw `type_expr` (no qualified / +> `Closure` / parameterized continuation). Resolution skips the builtin +> classifier (`TypeResolver.resolveNamed`'s `skip_builtin`, threaded from +> `te.is_raw` in [src/ir/lower.zig] and [src/ir/type_bridge.zig]) and looks up +> a `` `s2 ``-declared type (struct / enum / union / alias), else a NORMAL +> "unknown type 's2'" error (`UnknownTypeChecker.reportIfUnknownType` skips the +> builtin-name exemption when raw). A bare `s2` in type position is still the +> builtin int. +> - **Declaration position.** A bare reserved-name declaration of EVERY kind +> still errors (issue 0076 preserved); the backtick form is exempt. The check +> and the exemption are made structurally symmetric: +> `checkBindingName` / `checkDeclName` ([src/ir/semantic_diagnostics.zig]) take +> `is_raw` as a REQUIRED argument and skip inside the check — no call site can +> validate a name without also honoring the exemption, which is what kept the +> two from desyncing across the earlier attempts. > 2. **`#import c` foreign-name exemption.** `c_import.zig` synthesizes foreign -> `#foreign` decls with `Param.is_raw = true`, so generated C param names that -> collide with reserved type names (`s1`, `s2`) import unedited. +> `#foreign` decls with `Param.is_raw = true` (and the synthesized `FnDecl` +> `is_raw = true`), so generated C names that collide with reserved type names +> (`s1`, `s2`) import unedited and a reserved-name foreign fn is bare-callable. > -> **Boundary rules.** A raw identifier is a value name and is NEVER a type: using -> one in type position (`x : `s2 = 1`) is a clean parse error ([src/parser.zig] -> `parseTypeExpr` atom). A reserved-spelled FUNCTION (backtick-declared or -> `#import c` foreign) is bare-callable: `lowerCall` rewrites a `.type_expr` callee -> to an identifier when a function **of RAW provenance** of that name is in scope -> ([src/ir/lower.zig]) — the rewrite is scoped to the callee `FnDecl`'s `is_raw` -> flag (F0.6 attempt-3), so it only ever fires for a backtick / `#import c` foreign -> fn (the decl check guarantees no bare reserved-name fn exists), so `s2(4)` -> resolves to the function (`TypeName(val)` is not a cast). A later BARE -> reference in value position resolves to the binding; a bare `s2` in type position -> is still the type. +> **Bare-callable foreign / backtick fn.** `lowerCall` rewrites a `.type_expr` +> callee to an identifier when a function **of RAW provenance** of that name is in +> scope ([src/ir/lower.zig]) — scoped to the callee `FnDecl`'s `is_raw` flag, so it +> only ever fires for a backtick / `#import c` foreign fn (the decl check guarantees +> no bare reserved-name fn exists). `s2(4)` resolves to the function (`TypeName(val)` +> is not a cast). > -> A *bare* reserved-name binding in sx still errors (issue 0076 preserved): the -> `is_raw`-gated skip only fires for backtick / foreign names. Regression tests: -> `examples/0151-types-backtick-raw-identifier.sx` (backtick, decl positions), -> `examples/0152-types-backtick-control-flow.sx` (every control-flow/capture form -> + bare ref/call/member access), `examples/1054-errors-backtick-reserved-binding.sx` -> (`catch`/`onfail` tag bindings), `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` -> (foreign param + function-name exemption, bare-callable foreign fn), -> `examples/1139-diagnostics-backtick-raw-not-a-type.sx` (negative — raw in type -> position), `examples/1119`/`1121`/`1123` (negative — bare reserved binding still -> rejected across all forms), -> `examples/0153-types-backtick-const-fn-decl.sx` (positive — backtick `::` const + -> function decl, bare + backtick call), and -> `examples/1140-diagnostics-reserved-name-const-fn-decl.sx` (negative — bare `::` -> const + function decl rejected). Backtick lexer unit tests in `src/lexer.zig`. +> **Regression tests.** `examples/0151-types-backtick-raw-identifier.sx` (every +> VALUE position), `examples/0152-types-backtick-control-flow.sx` (every +> control-flow / capture form), `examples/0153-types-backtick-const-fn-decl.sx` +> (backtick `::` const + fn decl, bare + backtick call), +> `examples/0154-types-backtick-raw-type-reference.sx` (raw in TYPE position — +> struct / enum / union / alias decl + reference; bare `s2` still the int), +> `examples/0155-types-backtick-typed-const-union-tag.sx` (typed const + union tag), +> `examples/1054-errors-backtick-reserved-binding.sx` (`catch`/`onfail` tag +> bindings), `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` (foreign +> param + fn-name exemption, bare-callable foreign fn); negatives +> `examples/1119`/`1121`/`1123` (bare reserved binding across forms), +> `examples/1140-diagnostics-reserved-name-const-fn-decl.sx` (bare const + fn decl), +> `examples/1141-diagnostics-reserved-name-type-decl.sx` (bare struct / enum / union +> / error / typed-const decl). Backtick lexer + `resolveNamed(skip_builtin)` unit +> tests in `src/lexer.zig` / `src/ir/type_resolver.test.zig`. > > The original report is preserved below. diff --git a/readme.md b/readme.md index 5473311..e4f6fc0 100644 --- a/readme.md +++ b/readme.md @@ -105,26 +105,31 @@ y : s32 = 0; // explicit type z : s32 = ---; // uninitialized ``` -Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and can't be used -as bare identifiers at **any** binding site — a value binding (`:=` / typed local / -parameter), a `::` constant or function declaration, or a `::` type declaration -(`struct` / `enum` / `union` / alias / `protocol` / …) — each is an error -(`s2 :: 5` and `s2 :: (n) { … }` are rejected just like `s2 := 5`). A leading -backtick at the binding site escapes one into a raw identifier — its text drops the -backtick and it's never read as a type — so reserved spellings (and keywords) work -as ordinary names. The backtick is needed only where the name is declared; a later -bare reference in value position resolves to the binding, while a bare `s2` in type -position is still the type. It works in every identifier position (local, global, -parameter, field, function name, constant, and the control-flow / capture / binding -forms — destructure, `if`/`while` binding, `for` capture, match capture, -`catch`/`onfail` tag), and a reserved-spelled function is bare-callable: +Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare* +spelling can't be used as an identifier at **any** binding site — a value binding +(`:=` / typed local / parameter), a `::` constant or function declaration, or a +`::` type declaration (`struct` / `enum` / `union` / alias / `protocol` / …) — each +is an error (`s2 :: 5` and `s2 :: (n) { … }` are rejected just like `s2 := 5`). A +leading backtick escapes one into a **raw identifier**: `` `name `` is the literal +identifier `name` (the backtick drops out of the text), usable in **every** +position — value, declaration, and type. It is the only way handwritten sx can +spell a reserved name. ```sx -`s2 := 2.5; // value identifier "s2", distinct from the s2 type -print("{}\n", `s2); // 2.5 (or bare `s2`) +`s2 := 2.5; // identifier "s2", distinct from the s2 type +print("{}\n", `s2); // 2.5 (or bare `s2` in value position) + +`s2 :: struct { x: s64; } // declare a type named with a reserved spelling +v : `s2 = ---; // and reference it as a type — resolves to the struct +x : s2 = 3; // bare `s2` in type position is still the int type ``` -A raw identifier is a value name, never a type — `x : `s2 = 1` is an error. +It works in every identifier position — local, global, parameter, struct field, +union tag, function name, type/alias/import name, constant, and the control-flow / +capture / binding forms (destructure, `if`/`while` binding, `for` capture, match +capture, `catch`/`onfail` tag) — and a reserved-spelled function is bare-callable +(`s2(10)`). A backtick name used as a type resolves to a `` `name ``-declared type, +else a normal `unknown type` error. Foreign declarations from `#import c { … }` are exempt automatically: C names that collide with reserved type names (e.g. `s1`, `s2`) import unedited, and a foreign diff --git a/specs.md b/specs.md index fe69040..56ca1d2 100644 --- a/specs.md +++ b/specs.md @@ -38,34 +38,49 @@ sole exception: a `#builtin` constant defines the reserved type and is allowed.) #### Backtick raw-identifier escape -A leading backtick makes the following identifier **raw**: its text excludes the -backtick and it is never type-classified, so a reserved-type-name spelling can be -used as an ordinary value identifier. The backtick is required at the **binding -site** — the declaration that introduces the name — to escape the reserved-name -rule. A later reference is resolved by position: in **value** position a bare `s2` -resolves to the binding; in **type** position a bare `s2` is still the signed-int -type. +A leading backtick makes the following token a **raw identifier**: `` `name `` is +the **literal identifier** `name` — "treat this token as a plain identifier, never +the reserved keyword/type." The backtick is not part of the name's text (the text +is `name`), and the escape is usable in **every position**: value, declaration, +**and type**. It is the only way handwritten sx can spell a reserved name. ```sx -`s2 := 2.5; // OK — value identifier "s2", distinct from the s2 type +`s2 := 2.5; // OK — identifier "s2", distinct from the s2 type print("{}\n", `s2); // 2.5 (backtick reference) -print("{}\n", s2); // 2.5 (bare reference, resolves to the binding) -x : s2 = 3; // bare `s2` in TYPE position is still the s2 type +print("{}\n", s2); // 2.5 (bare reference in value position → the binding) +x : s2 = 3; // bare `s2` in TYPE position is still the s2 int type ``` -A raw identifier is a value name and is **never a type**: using one in type -position (`x : `s2 = 1`) is a parse error. +**Type position.** A backtick in type position is the literal name used as a type +reference: it resolves to a `` `s2 ``-declared type (struct / enum / union / type +alias / …), and never the builtin. A bare `s2` in type position stays the builtin +int; a backtick name with no matching declaration is a normal `unknown type 's2'` +error. -The escape works in **every identifier position** — local, global, parameter, -struct field, function name, a later reference, and every control-flow / capture / -binding form: a destructure name, an `if` / `while` optional binding, a `for` -capture and index, a match-arm capture, and a `catch` / `onfail` tag binding: +```sx +`s2 :: struct { x: s64; } // declare a type whose name is a reserved spelling +v : `s2 = ---; // reference it as a type — resolves to the struct +v.x = 7; +x : s2 = 3; // bare `s2` is still the 2-bit signed int +``` + +**Declaration position.** A *bare* reserved-name declaration of every kind still +errors (a value binding, a `::` constant / function, and a `::` type / alias / +protocol / foreign-class / ufcs / namespaced-import name); the backtick form is +exempt. The escape works in **every identifier position** — local, global, +parameter, struct field, union tag, function name, type/alias/import name, a later +reference, and every control-flow / capture / binding form (destructure name, +`if` / `while` optional binding, `for` capture and index, match-arm capture, and a +`catch` / `onfail` tag binding): ```sx `u8 := 100; // global `s2 :: 2.5; // constant declaration +`s2 : s64 : 5; // typed constant declaration `u8 :: (`s1: s64) -> s64 { `s1 } // function name + parameter P :: struct { `s2: f64; } // struct field +M :: union { `s1: s32; } // union tag +`u16 :: enum { A; B; } // type-declaration name `u8, rest := pair(); // destructure name if `s16 := maybe() { } // optional binding for xs: (`bool, `u16) { } // for capture + index @@ -73,8 +88,8 @@ x catch `s2 { } // catch tag binding ``` A reserved-spelled **function** is bare-callable: `` `s2 :: (n: s64) -> s64 { … } `` -can be invoked as `s2(10)` (the callee spelling parses as a type but resolves to -the function when one of that name is in scope; `TypeName(val)` is not a cast). +can be invoked as `s2(10)` (the bare callee spelling parses as a type but resolves +to the function when one of that name is in scope; `TypeName(val)` is not a cast). A backtick may also escape a keyword spelling (`` `for ``, `` `struct ``), yielding an identifier with that text. diff --git a/src/ast.zig b/src/ast.zig index d3b49de..f3c3541 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -380,12 +380,19 @@ pub const EnumDecl = struct { is_flags: bool = false, variant_values: []const ?*Node = &.{}, // explicit value per variant (null = auto), empty = all auto backing_type: ?*Node = null, // optional backing type: enum u8 { ... } + /// True when the declared NAME was a backtick raw identifier + /// (`` `s2 :: enum { … } ``) — exempt from the reserved-type-name decl + /// check (issue 0089). A bare reserved-name decl still errors. + is_raw: bool = false, }; pub const UnionDecl = struct { name: []const u8, field_names: []const []const u8, field_types: []const *Node, + /// True when the declared NAME was a backtick raw identifier — exempt from + /// the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; /// `Foo :: error { TagA, TagB }` — a named error set. Tags are bare @@ -393,6 +400,9 @@ pub const UnionDecl = struct { pub const ErrorSetDecl = struct { name: []const u8, tag_names: []const []const u8, + /// True when the declared NAME was a backtick raw identifier — exempt from + /// the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const StructTypeParam = struct { @@ -418,6 +428,10 @@ pub const StructDecl = struct { using_entries: []const UsingEntry = &.{}, methods: []const *Node = &.{}, // fn_decl nodes for struct methods constants: []const *Node = &.{}, // const_decl nodes for struct-level constants + /// True when the declared NAME was a backtick raw identifier + /// (`` `s2 :: struct { … } ``) — exempt from the reserved-type-name decl + /// check (issue 0089). A bare reserved-name decl still errors. + is_raw: bool = false, }; pub const StructFieldInit = struct { @@ -444,6 +458,12 @@ pub const TypeExpr = struct { name: []const u8, is_generic: bool = false, protocol_constraints: []const []const u8 = &.{}, // e.g. ["Eq", "Hashable"] for $T/Eq/Hashable + /// True when written as a backtick raw identifier in type position + /// (`` `s2 ``). Such a reference is the LITERAL name `s2` used as a type — + /// resolution skips the builtin/reserved classifier and looks up a + /// `` `s2 ``-declared type (struct/enum/union/alias), else "unknown type" + /// (issue 0089). A bare `s2` keeps `is_raw = false` and is the int type. + is_raw: bool = false, }; /// `$[]` in type position. Resolves to the i-th @@ -530,6 +550,10 @@ pub const ReturnStmt = struct { pub const ImportDecl = struct { path: []const u8, name: ?[]const u8, + /// True when the namespace NAME was a backtick raw identifier + /// (`` `s2 :: #import "…" ``) — exempt from the reserved-type-name decl + /// check (issue 0089). A flat `#import` (name == null) binds nothing. + is_raw: bool = false, }; pub const ArrayTypeExpr = struct { @@ -638,6 +662,9 @@ pub const SpreadExpr = struct { pub const NamespaceDecl = struct { name: []const u8, decls: []const *Node, + /// True when the namespace NAME was a backtick raw identifier — exempt + /// from the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const ForeignExpr = struct { @@ -648,6 +675,9 @@ pub const ForeignExpr = struct { pub const LibraryDecl = struct { lib_name: []const u8, name: []const u8, // sx-side constant name + /// True when the constant NAME was a backtick raw identifier — exempt from + /// the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const FrameworkDecl = struct { @@ -691,6 +721,9 @@ pub const TupleElement = struct { pub const UfcsAlias = struct { name: []const u8, target: []const u8, + /// True when the alias NAME was a backtick raw identifier — exempt from + /// the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const CImportDecl = struct { @@ -700,6 +733,9 @@ pub const CImportDecl = struct { flags: []const []const u8, name: ?[]const u8 = null, bitcode_paths: []const []const u8 = &.{}, // populated during import resolution + /// True when the namespace NAME was a backtick raw identifier — exempt + /// from the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const ProtocolMethodDecl = struct { @@ -720,6 +756,9 @@ pub const ProtocolDecl = struct { methods: []const ProtocolMethodDecl, is_inline: bool = false, // #inline — embedded fn ptrs instead of vtable pointer type_params: []const StructTypeParam = &.{}, // for `protocol(Target: Type) { ... }` + /// True when the declared NAME was a backtick raw identifier — exempt from + /// the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const ForeignRuntime = enum { @@ -776,6 +815,9 @@ pub const ForeignClassDecl = struct { members: []const ForeignClassMember = &.{}, is_foreign: bool = false, // `#foreign #...` prefix — class is provided by the foreign runtime; we only reference it is_main: bool = false, // `#jni_main` / `#objc_main` — class is the launchable entry (Activity / UIApplicationDelegate / ...) + /// True when the sx-side alias NAME was a backtick raw identifier — exempt + /// from the reserved-type-name decl check (issue 0089). + is_raw: bool = false, }; pub const JniEnvBlock = struct { diff --git a/src/imports.zig b/src/imports.zig index 5ce9de5..6afe18d 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -354,6 +354,7 @@ pub const ResolvedModule = struct { name: []const u8, other: ResolvedModule, span: ast.Span, + is_raw: bool, ) !void { const ns_node = try allocator.create(Node); ns_node.* = .{ @@ -361,6 +362,10 @@ pub const ResolvedModule = struct { .data = .{ .namespace_decl = .{ .name = name, .decls = other.decls, + // Carry the backtick raw escape from the `name :: #import …` + // form so a reserved-name namespace is exempt from the decl + // check, symmetric to every other decl site (issue 0089). + .is_raw = is_raw, } }, }; try self.scope.put(name, {}); @@ -487,6 +492,7 @@ pub fn resolveImports( .data = .{ .namespace_decl = .{ .name = ns_name, .decls = try ns_decls.toOwnedSlice(allocator), + .is_raw = ci.is_raw, } }, }; ns_node.source_file = file_path; @@ -569,7 +575,7 @@ pub fn resolveImports( }; if (imp.name) |ns_name| { - try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span); + try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span, imp.is_raw); } else { try mod.mergeFlat(allocator, &decl_list, &seen_in_list, &seen_nodes, imported_mod); } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index aa536b9..79bc7d3 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -11892,8 +11892,8 @@ pub const Lowering = struct { // type_bridge, which now takes the alias map as an explicit argument // (the `TypeTable.aliases` borrow is gone, A2.3). switch (node.data) { - .type_expr => |te| return self.typeResolver().resolveName(te.name), - .identifier => |id| return self.typeResolver().resolveName(id.name), + .type_expr => |te| return self.typeResolver().resolveName(te.name, te.is_raw), + .identifier => |id| return self.typeResolver().resolveName(id.name, id.is_raw), // A non-spread tuple literal in a type position is a tuple-type // literal (`(s32, s32)`); validate its elements are types and reject // non-type elements loudly (issue 0067). diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 9918099..9dacdb4 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -116,13 +116,16 @@ pub const UnknownTypeChecker = struct { if (node.source_file) |sf| self.diagnostics.current_source_file = sf; switch (node.data) { // ── Binding-introducing nodes: check the name(s), then recurse. ── + // Every site passes the node's own `is_raw` straight to the check — + // never an `if (!is_raw)` call-site guard — so the check and its + // exemption are one operation that cannot be threaded apart (0089). .var_decl => |vd| { - if (!vd.is_raw) self.checkBindingName(vd.name, vd.name_span); + self.checkBindingName(vd.name, vd.name_span, vd.is_raw); if (vd.value) |v| self.checkBindingNames(v); }, .destructure_decl => |dd| { for (dd.names, dd.name_spans, dd.name_is_raw) |n, sp, raw| { - if (!raw) self.checkBindingName(n, sp); + self.checkBindingName(n, sp, raw); } self.checkBindingNames(dd.value); }, @@ -131,7 +134,7 @@ pub const UnknownTypeChecker = struct { // `s2 :: (…) {…}` (free fn or struct/impl method) is rejected, // exactly like `s2 := …`. Backtick (`` `s2 :: … ``) and // `#import c` foreign fns set `is_raw` and are exempt (0089). - if (!fd.is_raw) self.checkBindingName(fd.name, fd.name_span); + self.checkBindingName(fd.name, fd.name_span, fd.is_raw); self.checkParamNames(fd.params); self.checkBindingNames(fd.body); }, @@ -140,29 +143,23 @@ pub const UnknownTypeChecker = struct { self.checkBindingNames(lm.body); }, .param => |p| { - if (!p.is_raw) self.checkBindingName(p.name, p.name_span); + self.checkBindingName(p.name, p.name_span, p.is_raw); if (p.default_expr) |de| self.checkBindingNames(de); }, .if_expr => |ie| { - if (ie.binding_name) |bn| { - if (!ie.binding_is_raw) self.checkBindingName(bn, ie.binding_span); - } + if (ie.binding_name) |bn| self.checkBindingName(bn, ie.binding_span, ie.binding_is_raw); self.checkBindingNames(ie.condition); self.checkBindingNames(ie.then_branch); if (ie.else_branch) |e| self.checkBindingNames(e); }, .while_expr => |we| { - if (we.binding_name) |bn| { - if (!we.binding_is_raw) self.checkBindingName(bn, we.binding_span); - } + if (we.binding_name) |bn| self.checkBindingName(bn, we.binding_span, we.binding_is_raw); self.checkBindingNames(we.condition); self.checkBindingNames(we.body); }, .for_expr => |fe| { - if (fe.capture_name.len != 0 and !fe.capture_is_raw) self.checkBindingName(fe.capture_name, fe.capture_span); - if (fe.index_name) |idx| { - if (!fe.index_is_raw) self.checkBindingName(idx, fe.index_span); - } + if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span, fe.capture_is_raw); + if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span, fe.index_is_raw); self.checkBindingNames(fe.iterable); if (fe.range_end) |re| self.checkBindingNames(re); self.checkBindingNames(fe.body); @@ -170,31 +167,23 @@ pub const UnknownTypeChecker = struct { .match_expr => |me| { self.checkBindingNames(me.subject); for (me.arms) |arm| { - if (arm.capture) |cap| { - if (!arm.capture_is_raw) self.checkBindingName(cap, arm.capture_span); - } + if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span, arm.capture_is_raw); if (arm.pattern) |p| self.checkBindingNames(p); self.checkBindingNames(arm.body); } }, .match_arm => |arm| { - if (arm.capture) |cap| { - if (!arm.capture_is_raw) self.checkBindingName(cap, arm.capture_span); - } + if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span, arm.capture_is_raw); if (arm.pattern) |p| self.checkBindingNames(p); self.checkBindingNames(arm.body); }, .catch_expr => |ce| { - if (ce.binding) |b| { - if (!ce.binding_is_raw) self.checkBindingName(b, ce.binding_span); - } + if (ce.binding) |b| self.checkBindingName(b, ce.binding_span, ce.binding_is_raw); self.checkBindingNames(ce.operand); self.checkBindingNames(ce.body); }, .onfail_stmt => |os| { - if (os.binding) |b| { - if (!os.binding_is_raw) self.checkBindingName(b, os.binding_span); - } + if (os.binding) |b| self.checkBindingName(b, os.binding_span, os.binding_is_raw); self.checkBindingNames(os.body); }, // impl / protocol-default / foreign-class method bodies: each @@ -203,12 +192,12 @@ pub const UnknownTypeChecker = struct { // param/local names mis-lower the same as any other. .impl_block => |ib| for (ib.methods) |m| self.checkBindingNames(m), .protocol_decl => |pd| { - self.checkDeclName(node, pd.name); + self.checkDeclName(node, pd.name, pd.is_raw); for (pd.methods) |m| { if (m.default_body) |body| { for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| { - if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue; - self.checkBindingName(pn, sp); + const raw = i < m.param_name_is_raw.len and m.param_name_is_raw[i]; + self.checkBindingName(pn, sp, raw); } self.checkBindingNames(body); } @@ -217,12 +206,12 @@ pub const UnknownTypeChecker = struct { .foreign_class_decl => |fcd| { // The sx-side alias (left of `::`) is a user-chosen name, so a // reserved spelling is rejected like any other type decl (0089). - self.checkDeclName(node, fcd.name); + self.checkDeclName(node, fcd.name, fcd.is_raw); for (fcd.members) |member| switch (member) { .method => |m| if (m.body) |body| { for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| { - if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue; - self.checkBindingName(pn, sp); + const raw = i < m.param_name_is_raw.len and m.param_name_is_raw[i]; + self.checkBindingName(pn, sp, raw); } self.checkBindingNames(body); }, @@ -235,7 +224,7 @@ pub const UnknownTypeChecker = struct { // module decls held inline; descend so an imported module's // reserved-name binding is rejected too (issue 0077). .namespace_decl => |nd| { - self.checkDeclName(node, nd.name); + self.checkDeclName(node, nd.name, nd.is_raw); for (nd.decls) |d| self.checkBindingNames(d); }, .const_decl => |cd| { @@ -247,12 +236,12 @@ pub const UnknownTypeChecker = struct { // own name on recursion — don't double-check it here (0089). switch (cd.value.data) { .builtin_expr, .struct_decl, .enum_decl, .union_decl, .error_set_decl, .fn_decl => {}, - else => if (!cd.is_raw) self.checkBindingName(cd.name, cd.name_span), + else => self.checkBindingName(cd.name, cd.name_span, cd.is_raw), } self.checkBindingNames(cd.value); }, .struct_decl => |sd| { - self.checkDeclName(node, sd.name); + self.checkDeclName(node, sd.name, sd.is_raw); for (sd.methods) |m| self.checkBindingNames(m); for (sd.constants) |c| self.checkBindingNames(c); for (sd.field_defaults) |fdef| if (fdef) |d| self.checkBindingNames(d); @@ -319,13 +308,13 @@ pub const UnknownTypeChecker = struct { // spelling as the declared name is rejected (issue 0089). These // have no nested binding sites, so only the name is checked. A // flat `#import`/`#import c` (name == null) binds nothing. ── - .enum_decl => |ed| self.checkDeclName(node, ed.name), - .union_decl => |ud| self.checkDeclName(node, ud.name), - .error_set_decl => |esd| self.checkDeclName(node, esd.name), - .ufcs_alias => |ua| self.checkDeclName(node, ua.name), - .library_decl => |ld| self.checkDeclName(node, ld.name), - .import_decl => |imp| if (imp.name) |n| self.checkDeclName(node, n), - .c_import_decl => |cid| if (cid.name) |n| self.checkDeclName(node, n), + .enum_decl => |ed| self.checkDeclName(node, ed.name, ed.is_raw), + .union_decl => |ud| self.checkDeclName(node, ud.name, ud.is_raw), + .error_set_decl => |esd| self.checkDeclName(node, esd.name, esd.is_raw), + .ufcs_alias => |ua| self.checkDeclName(node, ua.name, ua.is_raw), + .library_decl => |ld| self.checkDeclName(node, ld.name, ld.is_raw), + .import_decl => |imp| if (imp.name) |n| self.checkDeclName(node, n, imp.is_raw), + .c_import_decl => |cid| if (cid.name) |n| self.checkDeclName(node, n, cid.is_raw), // ── Leaves & pure type-expression nodes: no binding sites below. ── // Type-expression subtrees carry only type names (no value // bindings). Listing each tag explicitly (rather than an `else`) is @@ -370,8 +359,9 @@ pub const UnknownTypeChecker = struct { fn checkParamNames(self: UnknownTypeChecker, params: []const ast.Param) void { for (params) |p| { // A backtick raw param (`` (`s2: T) ``) or a `#import c` foreign - // param is exempt from the reserved-type-name rule (issue 0089). - if (!p.is_raw) self.checkBindingName(p.name, p.name_span); + // param is exempt from the reserved-type-name rule (issue 0089) — + // the exemption is honored inside `checkBindingName` via `p.is_raw`. + self.checkBindingName(p.name, p.name_span, p.is_raw); if (p.default_expr) |de| self.checkBindingNames(de); } } @@ -708,8 +698,8 @@ pub const UnknownTypeChecker = struct { switch (node.data) { // A `$`-prefixed name (`-> $R`) introduces/references a generic type // param inline — always valid in a type position. - .type_expr => |te| if (!te.is_generic) self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals), - .identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals), + .type_expr => |te| if (!te.is_generic) self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals, te.is_raw), + .identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals, id.is_raw), .pointer_type_expr => |pt| self.checkTypeNodeForUnknown(pt.pointee_type, declared, in_scope, type_vals), .many_pointer_type_expr => |mp| self.checkTypeNodeForUnknown(mp.element_type, declared, in_scope, type_vals), .slice_type_expr => |st| self.checkTypeNodeForUnknown(st.element_type, declared, in_scope, type_vals), @@ -753,11 +743,17 @@ pub const UnknownTypeChecker = struct { declared: *std.StringHashMap(void), in_scope: []const ast.StructTypeParam, type_vals: []const []const u8, + is_raw: bool, ) void { // Only bare identifiers are validated. Inline-spelled compound types // (`[:0]u8`, `mod.Type`, …) carry non-identifier characters — trust them. if (!isIdentLike(name)) return; - if (isBuiltinTypeName(name)) return; + // A backtick raw reference (`` `s2 ``) is the LITERAL name used as a + // type — explicitly NOT the builtin/reserved spelling — so it must + // resolve to a `` `s2 ``-declared type, else a normal "unknown type" + // error. Skip the builtin-name exemption that would otherwise wave a + // bare `s2` through (issue 0089). + if (!is_raw and isBuiltinTypeName(name)) return; for (in_scope) |tp| if (std.mem.eql(u8, tp.name, name)) return; if (declared.contains(name)) return; // Registered as a real (non-stub) type — covers imported concrete @@ -789,7 +785,14 @@ pub const UnknownTypeChecker = struct { /// (LLVM verifier abort, or a silent mutation-losing copy). Rejecting the /// name here, before lowering, keeps the `.identifier`-only address-of paths /// correct without any lowering special-case. - fn checkBindingName(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span) void { + /// `is_raw` is a REQUIRED argument, not a call-site guard: the exemption + /// lives INSIDE the check so no caller can validate a name without also + /// honoring the backtick / `#import c` foreign exemption. This is what keeps + /// the check and the exemption from desyncing — the recurring failure of the + /// earlier attempts, where each site threaded an `if (!is_raw)` guard + /// separately and one was forgotten (issue 0089). + fn checkBindingName(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span, is_raw: bool) void { + if (is_raw) return; if (isReservedTypeName(name)) self.diagnostics.addFmt(.err, span, "'{s}' is a reserved type name and cannot be used as an identifier", .{name}); } @@ -798,12 +801,14 @@ pub const UnknownTypeChecker = struct { /// identifier but carries no dedicated `name_span` field — struct / enum / /// union / error-set / protocol / foreign-class type decls, ufcs aliases, /// and namespaced imports (issue 0089). Each such node begins at its name - /// token, so the name's length isolates the caret onto the name. A - /// backtick raw / `#import c` foreign name never reaches here (those forms - /// are exempt at their own decl path). - fn checkDeclName(self: UnknownTypeChecker, node: *const Node, name: []const u8) void { + /// token (`createNode(name_start, …)`), so the name's length isolates the + /// caret onto the name — a single source for the span, no separate stored + /// field to drift from `node.span`. `is_raw` is REQUIRED, exactly as in + /// `checkBindingName`: a backtick raw / `#import c` foreign name is exempt + /// by construction. + fn checkDeclName(self: UnknownTypeChecker, node: *const Node, name: []const u8, is_raw: bool) void { const span = ast.Span{ .start = node.span.start, .end = node.span.start + @as(u32, @intCast(name.len)) }; - self.checkBindingName(name, span); + self.checkBindingName(name, span, is_raw); } }; diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index eef4c67..3d87458 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -107,8 +107,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap const n = node orelse return .unresolved; const si = StatelessInner{ .table = table, .alias_map = alias_map, .consts = consts }; return switch (n.data) { - .type_expr => |te| resolveTypeName(te.name, table, alias_map), - .identifier => |id| resolveTypeName(id.name, table, alias_map), + .type_expr => |te| resolveTypeName(te.name, table, alias_map, te.is_raw), + .identifier => |id| resolveTypeName(id.name, table, alias_map, id.is_raw), // Structural shapes (`*T`/`[*]T`/`[]T`/`?T`/`[N]T`, functions, plain // closures, plain tuples) are owned by the single canonical // `TypeResolver.resolveCompound` — no independent compound algorithm @@ -174,8 +174,9 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap /// Resolve a bare type name. The algorithm lives in `type_resolver.zig` /// (`TypeResolver.resolveNamed`, the single source); `type_bridge` forwards the /// caller-threaded `alias_map` (the single-source `ProgramIndex.type_alias_map`). -fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap) TypeId { - return type_resolver.TypeResolver.resolveNamed(name, table, alias_map); +/// `skip_builtin` carries the backtick raw escape (issue 0089). +fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap, skip_builtin: bool) TypeId { + return type_resolver.TypeResolver.resolveNamed(name, table, alias_map, skip_builtin); } /// Builtin primitive keyword → TypeId. The keyword table now lives in @@ -535,7 +536,7 @@ fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId /// resolves to the same empty inferred set, which is correct while no /// function raises (E1.3+). fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - if (ete.name) |name| return resolveTypeName(name, table, alias_map); + if (ete.name) |name| return resolveTypeName(name, table, alias_map, false); // `!` is not a legal type/identifier name, so this reserved StringId can // never collide with a user-declared set. const name_id = table.internString("!"); diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig index 51b0208..70e895d 100644 --- a/src/ir/type_resolver.test.zig +++ b/src/ir/type_resolver.test.zig @@ -144,21 +144,33 @@ test "TypeResolver.resolveName resolves aliases via ProgramIndex (not the TypeTa try index.type_alias_map.put("NodeRef", ptr_s64); // alias → pointer const tr = TypeResolver{ .alloc = alloc, .types = &table, .diagnostics = null, .index = &index }; - try std.testing.expectEqual(@as(TypeId, .u32), tr.resolveName("ShaderHandle")); - try std.testing.expectEqual(ptr_s64, tr.resolveName("NodeRef")); + try std.testing.expectEqual(@as(TypeId, .u32), tr.resolveName("ShaderHandle", false)); + try std.testing.expectEqual(ptr_s64, tr.resolveName("NodeRef", false)); // Primitive is checked before alias. - try std.testing.expectEqual(@as(TypeId, .s64), tr.resolveName("s64")); + try std.testing.expectEqual(@as(TypeId, .s64), tr.resolveName("s64", false)); } test "TypeResolver.resolveNamed: width-int, string-prefix, unknown→stub" { const alloc = std.testing.allocator; var table = TypeTable.init(alloc); defer table.deinit(); - try std.testing.expectEqual(table.intern(.{ .signed = 7 }), TypeResolver.resolveNamed("s7", &table, null)); - try std.testing.expectEqual(table.ptrTo(.s64), TypeResolver.resolveNamed("*s64", &table, null)); + try std.testing.expectEqual(table.intern(.{ .signed = 7 }), TypeResolver.resolveNamed("s7", &table, null, false)); + try std.testing.expectEqual(table.ptrTo(.s64), TypeResolver.resolveNamed("*s64", &table, null, false)); // Unknown name, no alias map → empty-struct stub (preserved behavior; // never `.unresolved`, which is reserved for failed *generic* resolution). - try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null) != .unresolved); + try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null, false) != .unresolved); +} + +test "TypeResolver.resolveNamed: skip_builtin resolves a raw reserved-name type, not the builtin" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + // A registered user type named "s2" (a reserved int spelling). + const name_id = table.internString("s2"); + const user_s2 = table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); + // Bare lookup → the builtin 2-bit signed int; raw lookup → the user type. + try std.testing.expectEqual(table.intern(.{ .signed = 2 }), TypeResolver.resolveNamed("s2", &table, null, false)); + try std.testing.expectEqual(user_s2, TypeResolver.resolveNamed("s2", &table, null, true)); } test "TypeResolver.parseWidthInt: every width 1..64, both signs; rejects out-of-range / non-int" { diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 3b673a1..a4cf58c 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -244,11 +244,21 @@ pub const TypeResolver = struct { /// `type_bridge` via the alias map threaded through `resolveAstType`. The /// stub fall-through preserves long-standing behavior for as-yet- /// unregistered names. - pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId { + /// + /// `skip_builtin` is the backtick raw-identifier escape (`` `s2 `` in type + /// position, issue 0089): a raw reference is the LITERAL name used as a + /// type, so it bypasses the builtin/reserved classifier and resolves only + /// through registered-type → alias → stub. A bare `s2` keeps the default + /// (`false`) and resolves to the builtin int type. The string-prefix + /// recursion always passes `false`: the inner names (`*T`/`?T`) are bare, + /// never raw. + pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId), skip_builtin: bool) TypeId { // Builtin primitive keyword or arbitrary-width integer (`s1`-`s64`, // `u1`-`u64`) — the single builtin classifier, also reused by the // numeric-limit accessor intercept. - if (resolveBuiltinName(name, table)) |id| return id; + if (!skip_builtin) { + if (resolveBuiltinName(name, table)) |id| return id; + } // Sentinel-terminated slice: [:0]u8 → string. if (name.len >= 5 and name[0] == '[' and name[1] == ':') { if (std.mem.indexOfScalar(u8, name, ']')) |close| { @@ -259,15 +269,15 @@ pub const TypeResolver = struct { } // Many-pointer: [*]T. if (name.len >= 4 and name[0] == '[' and name[1] == '*' and name[2] == ']') { - return table.manyPtrTo(resolveNamed(name[3..], table, alias_map)); + return table.manyPtrTo(resolveNamed(name[3..], table, alias_map, false)); } // Pointer: *T. if (name.len >= 2 and name[0] == '*') { - return table.ptrTo(resolveNamed(name[1..], table, alias_map)); + return table.ptrTo(resolveNamed(name[1..], table, alias_map, false)); } // Optional: ?T. if (name.len >= 2 and name[0] == '?') { - return table.optionalOf(resolveNamed(name[1..], table, alias_map)); + return table.optionalOf(resolveNamed(name[1..], table, alias_map, false)); } // Named struct/enum/union — already-registered wins, then alias, then // a fresh empty-struct stub for an as-yet-unregistered name. @@ -280,8 +290,9 @@ pub const TypeResolver = struct { } /// Resolve a bare type name through the canonical alias source - /// (`ProgramIndex.type_alias_map`). - pub fn resolveName(self: TypeResolver, name: []const u8) TypeId { - return resolveNamed(name, self.types, &self.index.type_alias_map); + /// (`ProgramIndex.type_alias_map`). `skip_builtin` carries the backtick raw + /// escape (issue 0089) — see `resolveNamed`. + pub fn resolveName(self: TypeResolver, name: []const u8, skip_builtin: bool) TypeId { + return resolveNamed(name, self.types, &self.index.type_alias_map, skip_builtin); } }; diff --git a/src/parser.zig b/src/parser.zig index 0ef7bc7..8ec384e 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -88,7 +88,7 @@ pub const Parser = struct { // Check for #import c { ... } (C import block) if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) { self.advance(); // consume 'c' - return self.parseCImportBlock(start, null); + return self.parseCImportBlock(start, null, false); } if (self.current.tag != .string_literal) { return self.fail("expected string path after '#import'"); @@ -183,7 +183,7 @@ pub const Parser = struct { // Check for name :: #import c { ... } if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) { self.advance(); // consume 'c' - return self.parseCImportBlock(start_pos, name); + return self.parseCImportBlock(start_pos, name, name_is_raw); } if (self.current.tag != .string_literal) { return self.fail("expected string path after '#import'"); @@ -192,7 +192,7 @@ pub const Parser = struct { const path = raw[1 .. raw.len - 1]; self.advance(); try self.expect(.semicolon); - return try self.createNode(start_pos, .{ .import_decl = .{ .path = path, .name = name } }); + return try self.createNode(start_pos, .{ .import_decl = .{ .path = path, .name = name, .is_raw = name_is_raw } }); } // Named library: name :: #library "libname"; @@ -205,7 +205,7 @@ pub const Parser = struct { const lib_name = raw[1 .. raw.len - 1]; self.advance(); try self.expect(.semicolon); - return try self.createNode(start_pos, .{ .library_decl = .{ .lib_name = lib_name, .name = name } }); + return try self.createNode(start_pos, .{ .library_decl = .{ .lib_name = lib_name, .name = name, .is_raw = name_is_raw } }); } // Compile-time evaluation: name :: #run expr; @@ -229,22 +229,22 @@ pub const Parser = struct { // Enum declaration if (self.current.tag == .kw_enum) { - return self.parseEnumDecl(name, start_pos); + return self.parseEnumDecl(name, start_pos, name_is_raw); } // Error-set declaration: name :: error { TagA, TagB } if (self.current.tag == .kw_error) { - return self.parseErrorSetDecl(name, start_pos); + return self.parseErrorSetDecl(name, start_pos, name_is_raw); } // Struct declaration if (self.current.tag == .kw_struct) { - return self.parseStructDecl(name, start_pos); + return self.parseStructDecl(name, start_pos, name_is_raw); } // Protocol declaration if (self.current.tag == .kw_protocol) { - return self.parseProtocolDecl(name, start_pos); + return self.parseProtocolDecl(name, start_pos, name_is_raw); } // Foreign-type binding with optional prefix modifiers: @@ -255,12 +255,12 @@ pub const Parser = struct { // `#foreign` flips that to "reference an existing class on the foreign side." // `#jni_main` flags the class as the launchable entry (Android Activity). if (self.tryParseForeignClassPrefix()) |prefix| { - return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main); + return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main, name_is_raw); } // C-style union declaration if (self.current.tag == .kw_union) { - return self.parseUnionDecl(name, start_pos); + return self.parseUnionDecl(name, start_pos, name_is_raw); } // UFCS alias: name :: ufcs target; @@ -272,7 +272,7 @@ pub const Parser = struct { const target = self.tokenSlice(self.current); self.advance(); try self.expect(.semicolon); - return try self.createNode(start_pos, .{ .ufcs_alias = .{ .name = name, .target = target } }); + return try self.createNode(start_pos, .{ .ufcs_alias = .{ .name = name, .target = target, .is_raw = name_is_raw } }); } // Function declaration: (params) -> type { body } or () { body } @@ -332,7 +332,7 @@ pub const Parser = struct { return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = value, .name_span = name_span, .is_raw = name_is_raw } }); } - fn parseCImportBlock(self: *Parser, start: u32, name: ?[]const u8) anyerror!*Node { + fn parseCImportBlock(self: *Parser, start: u32, name: ?[]const u8, name_is_raw: bool) anyerror!*Node { try self.expect(.l_brace); var includes = std.ArrayList([]const u8).empty; var sources = std.ArrayList([]const u8).empty; @@ -381,6 +381,7 @@ pub const Parser = struct { .defines = try defines.toOwnedSlice(self.allocator), .flags = try flags.toOwnedSlice(self.allocator), .name = name, + .is_raw = name_is_raw, } }); } @@ -394,7 +395,7 @@ pub const Parser = struct { self.advance(); const value = try self.parseExpr(); try self.expectSemicolonAfter(value); - return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = type_node, .value = value } }); + return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = type_node, .value = value, .name_span = name_span, .is_raw = name_is_raw } }); } if (self.current.tag == .equal) { @@ -629,11 +630,16 @@ pub const Parser = struct { } if (self.current.tag.isTypeKeyword() or self.isIdentLike()) { - // A backtick raw identifier (`` `s2 ``) is a VALUE-name escape; it is - // never a type. Reject it in type position rather than silently - // type-classifying it (issue 0089). + // A backtick raw identifier (`` `s2 ``) in type position is the + // LITERAL name `s2` used as a type reference — never the builtin / + // reserved keyword. It is always a plain named-type reference (no + // qualified-path, `Closure`, or parameterized continuation), so emit + // a raw `type_expr` and return; resolution skips the builtin + // classifier and looks up a `` `s2 ``-declared type (issue 0089). if (self.current.is_raw) { - return self.failFmt("`{s}` is a raw identifier, not a type — the backtick escape names a value, never a type", .{self.tokenSlice(self.current)}); + const raw_name = self.tokenSlice(self.current); + self.advance(); + return try self.createNode(start, .{ .type_expr = .{ .name = raw_name, .is_raw = true } }); } var name = self.tokenSlice(self.current); self.advance(); @@ -787,20 +793,20 @@ pub const Parser = struct { } // Inline struct type in type position: struct { ... } if (self.current.tag == .kw_struct) { - return try self.parseStructDecl("__anon", start); + return try self.parseStructDecl("__anon", start, false); } // Inline C-style union in type position: union { ... } if (self.current.tag == .kw_union) { - return try self.parseUnionDecl("__anon", start); + return try self.parseUnionDecl("__anon", start, false); } // Inline enum type in type position: enum { ... } if (self.current.tag == .kw_enum) { - return try self.parseEnumDecl("__anon", start); + return try self.parseEnumDecl("__anon", start, false); } return self.fail("expected type name"); } - fn parseEnumDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + fn parseEnumDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node { self.advance(); // skip 'enum' // Check for 'flags' modifier: enum flags { ... } @@ -874,10 +880,11 @@ pub const Parser = struct { .is_flags = is_flags, .variant_values = if (has_any_value) try variant_values.toOwnedSlice(self.allocator) else &.{}, .backing_type = backing_type, + .is_raw = name_is_raw, } }); } - fn parseErrorSetDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + fn parseErrorSetDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node { self.advance(); // skip 'error' try self.expect(.l_brace); var tag_names = std.ArrayList([]const u8).empty; @@ -899,10 +906,11 @@ pub const Parser = struct { return try self.createNode(start_pos, .{ .error_set_decl = .{ .name = name, .tag_names = try tag_names.toOwnedSlice(self.allocator), + .is_raw = name_is_raw, } }); } - fn parseUnionDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + fn parseUnionDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node { self.advance(); // skip 'union' try self.expect(.l_brace); var field_names = std.ArrayList([]const u8).empty; @@ -914,7 +922,7 @@ pub const Parser = struct { const anon_field = try std.fmt.allocPrint(self.allocator, "__anon_{d}", .{anon_idx}); anon_idx += 1; const anon_struct_name = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ name, anon_field }); - const struct_node = try self.parseStructDecl(anon_struct_name, self.current.loc.start); + const struct_node = try self.parseStructDecl(anon_struct_name, self.current.loc.start, false); try field_names.append(self.allocator, anon_field); try field_types.append(self.allocator, struct_node); if (self.current.tag == .semicolon) { @@ -942,10 +950,11 @@ pub const Parser = struct { .name = name, .field_names = try field_names.toOwnedSlice(self.allocator), .field_types = try field_types.toOwnedSlice(self.allocator), + .is_raw = name_is_raw, } }); } - fn parseStructDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + fn parseStructDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node { self.advance(); // skip 'struct' // Optional `#compiler` attribute: all methods inside this struct are @@ -1133,10 +1142,11 @@ pub const Parser = struct { .using_entries = try using_entries.toOwnedSlice(self.allocator), .methods = try methods.toOwnedSlice(self.allocator), .constants = try constants.toOwnedSlice(self.allocator), + .is_raw = name_is_raw, } }); } - fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node { self.advance(); // skip 'protocol' // Optional type params: protocol(Target: Type, U: Type) { ... } @@ -1249,6 +1259,7 @@ pub const Parser = struct { .methods = try methods.toOwnedSlice(self.allocator), .is_inline = is_inline, .type_params = try type_params.toOwnedSlice(self.allocator), + .is_raw = name_is_raw, } }); } @@ -1335,7 +1346,7 @@ pub const Parser = struct { }; } - fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool) anyerror!*Node { + fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool, name_is_raw: bool) anyerror!*Node { self.advance(); // skip directive token try self.expect(.l_paren); @@ -1576,6 +1587,7 @@ pub const Parser = struct { .members = try members.toOwnedSlice(self.allocator), .is_foreign = is_foreign, .is_main = is_main, + .is_raw = name_is_raw, } }); } @@ -2820,15 +2832,15 @@ pub const Parser = struct { }, .kw_struct => { // Anonymous struct expression: struct { value: T; count: u32; } - return try self.parseStructDecl("__anon", start); + return try self.parseStructDecl("__anon", start, false); }, .kw_enum => { // Anonymous enum expression: enum { variant: T; other: u32; } - return try self.parseEnumDecl("__anon", start); + return try self.parseEnumDecl("__anon", start, false); }, .kw_union => { // Anonymous C-style union expression: union { f: f32; i: s32; } - return try self.parseUnionDecl("__anon", start); + return try self.parseUnionDecl("__anon", start, false); }, .kw_if => { return self.parseIfExpr();