diff --git a/examples/0152-types-backtick-control-flow.sx b/examples/0152-types-backtick-control-flow.sx new file mode 100644 index 0000000..7b75860 --- /dev/null +++ b/examples/0152-types-backtick-control-flow.sx @@ -0,0 +1,57 @@ +// Backtick raw identifier across every control-flow / capture / binding form, +// plus bare later uses. A reserved type-name spelling (`s2`, `u8`, …) works as a +// binding name in a destructure, an `if`/`while` optional binding, a `for` +// capture + index, and a match-arm capture; a backtick-named function is +// bare-callable; and a backtick struct field is bare- or backtick-accessible. +// The escape is needed only at the binding site — a later BARE reference / call +// / member access resolves to the binding. A *bare* binding name is still the +// reserved type (see examples/1121), so the escape is the only way to spell +// these as values. +// Regression (issue 0089 — attempt-2 completeness across binding forms). +#import "modules/std.sx"; + +pair :: () -> (s64, s64) { (1, 2) } +maybe :: () -> ?s64 { return 42; } + +// Function named with a reserved spelling — bare-callable (no backtick at call). +`s2 :: (n: s64) -> s64 { return n + 1; } + +Quad :: struct { `s1: s32; `s2: s32; } + +main :: () -> s32 { + // destructure binding names + `u8, rest := pair(); + print("dstr = {} {}\n", `u8, rest); + + // if optional binding + bare-position reference inside the branch + if `s16 := maybe() { + print("if = {}\n", `s16); + } + + // while optional binding (name only — the while binding isn't body-exposed) + while `s32 := maybe() { + break; + } + + // for capture + index names + xs := [3]s64.{ 10, 20, 30 }; + for xs: (`bool, `u16) { + print("for = {} @ {}\n", `bool, `u16); + } + + // match-arm capture + opt: ?s64 = 5; + m := if opt == { + case .some: (`string) { `string * 2 } + case .none: { 0 } + }; + print("match = {}\n", m); + + // backtick function called BARE and via backtick — both resolve to the fn + print("call = {} {}\n", s2(10), `s2(10)); + + // struct field named with a reserved spelling: bare + backtick member access + q := Quad.{ `s1 = 7, `s2 = 9 }; + print("field = {} {} | {} {}\n", q.s1, q.s2, q.`s1, q.`s2); + return 0; +} diff --git a/examples/1054-errors-backtick-reserved-binding.sx b/examples/1054-errors-backtick-reserved-binding.sx new file mode 100644 index 0000000..d323ada --- /dev/null +++ b/examples/1054-errors-backtick-reserved-binding.sx @@ -0,0 +1,40 @@ +// Backtick raw identifier as the error-tag binding of `catch` and `onfail`. A +// reserved type-name spelling (`s2`, `u8`) is a value name when backticked, so +// it is accepted as the tag binding and a later reference resolves to it. A +// *bare* reserved spelling in the same position is still rejected (see +// examples/1123), so the backtick escape is the only way to spell these tags. +// Regression (issue 0089 — attempt-2 catch/onfail coverage). +#import "modules/std.sx"; + +E :: error { Bad, Empty } + +parse :: (n: s32) -> (s32, !E) { + if n < 0 { raise error.Bad; } + if n == 0 { raise error.Empty; } + return n * 2; +} + +// `catch` tag binding spelled `s2`, referenced in the match body. +classify :: (n: s32) -> s32 { + return parse(n) catch `s2 == { + case .Bad: 1; + case .Empty: 2; + else: 3 + }; +} + +// `onfail` tag binding spelled `u8`, referenced in the cleanup body. +cleanup :: (n: s32) -> !E { + onfail `u8 { if `u8 == error.Bad { print("cleanup: bad\n"); } } + if n < 0 { raise error.Bad; } + return; +} + +main :: () -> s32 { + print("classify(-1) = {}\n", classify(-1)); + print("classify(0) = {}\n", classify(0)); + print("classify(5) = {}\n", classify(5)); + c := cleanup(-1); + print("done\n"); + return 0; +} diff --git a/examples/1139-diagnostics-backtick-raw-not-a-type.sx b/examples/1139-diagnostics-backtick-raw-not-a-type.sx new file mode 100644 index 0000000..bfdb943 --- /dev/null +++ b/examples/1139-diagnostics-backtick-raw-not-a-type.sx @@ -0,0 +1,12 @@ +// 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/1220-ffi-c-import-reserved-name-params.c b/examples/1220-ffi-c-import-reserved-name-params.c index 560ab3b..120a0dd 100644 --- a/examples/1220-ffi-c-import-reserved-name-params.c +++ b/examples/1220-ffi-c-import-reserved-name-params.c @@ -7,3 +7,7 @@ int ffi_pick(int s1, int s2, int which) { int ffi_sum(int s1, int s2) { return s1 + s2; } + +int s2(int u8) { + return u8 + 100; +} diff --git a/examples/1220-ffi-c-import-reserved-name-params.h b/examples/1220-ffi-c-import-reserved-name-params.h index 33929c6..7c1bdea 100644 --- a/examples/1220-ffi-c-import-reserved-name-params.h +++ b/examples/1220-ffi-c-import-reserved-name-params.h @@ -1,5 +1,7 @@ -/* Foreign C declarations whose parameter names (`s1`, `s2`) collide with - sx's reserved signed-int type spellings. The `#import c` exemption must - accept these generated names unedited (issue 0089). */ +/* Foreign C declarations whose names collide with sx's reserved type spellings. + The `#import c` exemption must accept these generated names unedited, both as + parameter names (`s1`, `s2`) and as a FUNCTION name (`s2`) — and a foreign + reserved-name function must be bare-callable (issue 0089). */ int ffi_pick(int s1, int s2, int which); int ffi_sum(int s1, int s2); +int s2(int u8); diff --git a/examples/1220-ffi-c-import-reserved-name-params.sx b/examples/1220-ffi-c-import-reserved-name-params.sx index dd25659..f9f3284 100644 --- a/examples/1220-ffi-c-import-reserved-name-params.sx +++ b/examples/1220-ffi-c-import-reserved-name-params.sx @@ -1,9 +1,12 @@ -// `#import c` foreign-name exemption: a C header's parameter names `s1`/`s2` -// collide with sx's reserved signed-int type spellings. Foreign decls are -// treated as RAW — their names are never type-classified nor reserved-checked -// — so the generated `#foreign` bindings import and call without hand-edits -// (no backticks needed). Before issue 0089 this errored with "'s1' is a -// reserved type name and cannot be used as an identifier". +// `#import c` foreign-name exemption: C names that collide with sx's reserved +// type spellings import unedited. Foreign decls are treated as RAW — their names +// are never type-classified nor reserved-checked — so the generated `#foreign` +// bindings import and call without hand-edits (no backticks needed). This covers +// parameter names (`s1`/`s2`), a function whose own NAME is a reserved spelling +// (`s2`), and bare-calling that function (its callee spelling parses as a type +// but resolves to the foreign fn). Before issue 0089 the params errored with +// "'s1' is a reserved type name and cannot be used as an identifier", and the +// bare call errored with "unresolved 's2'". // Regression (issue 0089). #import "modules/std.sx"; @@ -16,5 +19,6 @@ main :: () -> s32 { print("pick(10,20,0) = {}\n", ffi_pick(10, 20, 0)); print("pick(10,20,1) = {}\n", ffi_pick(10, 20, 1)); print("sum(10,20) = {}\n", ffi_sum(10, 20)); + print("s2(4) bare = {}\n", s2(4)); 0 } diff --git a/examples/expected/0152-types-backtick-control-flow.exit b/examples/expected/0152-types-backtick-control-flow.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0152-types-backtick-control-flow.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0152-types-backtick-control-flow.stderr b/examples/expected/0152-types-backtick-control-flow.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0152-types-backtick-control-flow.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0152-types-backtick-control-flow.stdout b/examples/expected/0152-types-backtick-control-flow.stdout new file mode 100644 index 0000000..834e758 --- /dev/null +++ b/examples/expected/0152-types-backtick-control-flow.stdout @@ -0,0 +1,8 @@ +dstr = 1 2 +if = 42 +for = 10 @ 0 +for = 20 @ 1 +for = 30 @ 2 +match = 10 +call = 11 11 +field = 7 9 | 7 9 diff --git a/examples/expected/1054-errors-backtick-reserved-binding.exit b/examples/expected/1054-errors-backtick-reserved-binding.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/1054-errors-backtick-reserved-binding.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1054-errors-backtick-reserved-binding.stderr b/examples/expected/1054-errors-backtick-reserved-binding.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1054-errors-backtick-reserved-binding.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1054-errors-backtick-reserved-binding.stdout b/examples/expected/1054-errors-backtick-reserved-binding.stdout new file mode 100644 index 0000000..4a9a95f --- /dev/null +++ b/examples/expected/1054-errors-backtick-reserved-binding.stdout @@ -0,0 +1,5 @@ +classify(-1) = 1 +classify(0) = 2 +classify(5) = 10 +cleanup: bad +done diff --git a/examples/expected/1139-diagnostics-backtick-raw-not-a-type.exit b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stderr b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stderr new file mode 100644 index 0000000..8608dcb --- /dev/null +++ b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stderr @@ -0,0 +1,5 @@ +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.stdout b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1139-diagnostics-backtick-raw-not-a-type.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/1220-ffi-c-import-reserved-name-params.stdout b/examples/expected/1220-ffi-c-import-reserved-name-params.stdout index 78b2f72..7c90d43 100644 --- a/examples/expected/1220-ffi-c-import-reserved-name-params.stdout +++ b/examples/expected/1220-ffi-c-import-reserved-name-params.stdout @@ -1,3 +1,4 @@ pick(10,20,0) = 10 pick(10,20,1) = 20 sum(10,20) = 30 +s2(4) bare = 104 diff --git a/issues/0089-backtick-raw-identifier.md b/issues/0089-backtick-raw-identifier.md index 25304fe..ff0e275 100644 --- a/issues/0089-backtick-raw-identifier.md +++ b/issues/0089-backtick-raw-identifier.md @@ -4,25 +4,41 @@ > > 1. **Backtick raw identifier.** The lexer recognises a leading backtick > (`` `s2 ``) and emits an `.identifier` token whose span excludes the backtick, -> carrying a new `Token.is_raw` flag ([src/lexer.zig], [src/token.zig]). A raw +> 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 flag threads to `VarDecl.is_raw` / `Param.is_raw` -> ([src/ast.zig]) at the binding sites, and `UnknownTypeChecker` skips the -> reserved-name check for raw bindings ([src/ir/semantic_diagnostics.zig]). -> Because the token tag stays `.identifier`, the escape works in every position -> (local, global, param, field, function name, struct member, later reference) -> with no per-site parser change. +> 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). > 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. > +> **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 that name is in scope ([src/ir/lower.zig]), +> 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. +> > 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, every position), -> `examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}` (foreign exemption), -> `examples/1119-diagnostics-reserved-type-name-as-identifier.sx` (negative — -> bare binding still rejected). Backtick lexer unit tests in `src/lexer.zig`. +> `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). Backtick lexer unit tests in `src/lexer.zig`. > > The original report is preserved below. diff --git a/readme.md b/readme.md index a1ec6dd..79b91d8 100644 --- a/readme.md +++ b/readme.md @@ -106,17 +106,26 @@ z : s32 = ---; // uninitialized ``` Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and can't be used -as bare value identifiers. A leading backtick 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: +as bare value identifiers. 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, 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: ```sx `s2 := 2.5; // value identifier "s2", distinct from the s2 type -print("{}\n", `s2); // 2.5 +print("{}\n", `s2); // 2.5 (or bare `s2`) ``` +A raw identifier is a value name, never a type — `x : `s2 = 1` is an error. + Foreign declarations from `#import c { … }` are exempt automatically: C names that -collide with reserved type names (e.g. `s1`, `s2`) import unedited. +collide with reserved type names (e.g. `s1`, `s2`) import unedited, and a foreign +reserved-name function is bare-callable by its C name. ### Structs diff --git a/specs.md b/specs.md index fe0290b..864b0e2 100644 --- a/specs.md +++ b/specs.md @@ -29,32 +29,50 @@ s2 := 2.5; // ERROR: 's2' is a reserved type name and cannot be used as an ide 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 every occurrence -of that identifier (declaration and each reference); a *bare* `s2` is still the -signed-int type. +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. ```sx `s2 := 2.5; // OK — value identifier "s2", distinct from the s2 type -print("{}\n", `s2); // 2.5 +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 ``` -The escape works in every identifier position — local, global, parameter, struct -field, function name, and a later reference: +A raw identifier is a value name and is **never a type**: using one in type +position (`x : `s2 = 1`) is a parse 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 `u8 := 100; // global `s2 :: (`s1: s64) -> s64 { `s1 } // function name + parameter P :: struct { `s2: f64; } // struct field +`u8, rest := pair(); // destructure name +if `s16 := maybe() { } // optional binding +for xs: (`bool, `u16) { } // for capture + index +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). + A backtick may also escape a keyword spelling (`` `for ``, `` `struct ``), yielding an identifier with that text. **`#import c` exemption.** Foreign declarations synthesized by an `#import c { … }` -block are treated as raw automatically: a generated C parameter or name that -collides with a reserved type name (e.g. `s1`, `s2`) imports unedited, with no -backticks and no reserved-name error. The exemption is scoped to the foreign decls — -it does not make a foreign `s2` usable as the sx `s2` type, nor relax the rule for +block are treated as raw automatically: a generated C parameter or function name +that collides with a reserved type name (e.g. `s1`, `s2`) imports unedited, with no +backticks and no reserved-name error, and a foreign reserved-name function is +bare-callable by its C name. The exemption is scoped to the foreign decls — it does +not make a foreign `s2` usable as the sx `s2` type, nor relax the rule for hand-written sx code. ### Literals diff --git a/src/ast.zig b/src/ast.zig index f6c7251..23b7213 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -188,6 +188,10 @@ pub const StringLiteral = struct { pub const Identifier = struct { name: []const u8, + /// True when written as a backtick raw identifier (`` `s2 ``). Carried so a + /// destructure target (`` `s2, b := … ``) can be recognised as raw and + /// exempted from the reserved-type-name binding check (issue 0089). + is_raw: bool = false, }; pub const EnumLiteral = struct { @@ -277,6 +281,9 @@ pub const IfExpr = struct { is_comptime: bool = false, // true for `inline if` — compile-time branch elimination binding_name: ?[]const u8 = null, // for `if val := expr { ... }` optional binding binding_span: ?Span = null, // span of `binding_name` (set iff `binding_name` is) + /// True when the optional binding was a backtick raw identifier + /// (`` if `s2 := … ``) — exempt from the reserved-type-name check (issue 0089). + binding_is_raw: bool = false, }; pub const MatchExpr = struct { @@ -291,6 +298,9 @@ pub const MatchArm = struct { is_break: bool, capture: ?[]const u8 = null, // payload binding name: case .variant: (name) { ... } capture_span: ?Span = null, // span of `capture` (set iff `capture` is) + /// True when the capture was a backtick raw identifier + /// (`` case .v: (`s2) ``) — exempt from the reserved-type-name check (issue 0089). + capture_is_raw: bool = false, }; pub const ConstDecl = struct { @@ -341,6 +351,10 @@ pub const MultiAssign = struct { pub const DestructureDecl = struct { names: []const []const u8, name_spans: []const Span, // one per entry in `names`, same order + /// One per entry in `names`, same order: true when that target was a + /// backtick raw identifier (`` `s2, b := … ``) — exempt from the + /// reserved-type-name binding check (issue 0089). + name_is_raw: []const bool, value: *Node, }; @@ -462,6 +476,9 @@ pub const CatchExpr = struct { operand: *Node, binding: ?[]const u8 = null, binding_span: ?Span = null, // span of `binding` (set iff `binding` is) + /// True when the binding was a backtick raw identifier + /// (`` x catch `s2 { … } ``) — exempt from the reserved-type-name check (issue 0089). + binding_is_raw: bool = false, body: *Node, is_match_body: bool = false, }; @@ -472,6 +489,9 @@ pub const CatchExpr = struct { pub const OnFailStmt = struct { binding: ?[]const u8 = null, binding_span: ?Span = null, // span of `binding` (set iff `binding` is) + /// True when the binding was a backtick raw identifier + /// (`` onfail `s2 { … } ``) — exempt from the reserved-type-name check (issue 0089). + binding_is_raw: bool = false, body: *Node, }; @@ -566,6 +586,9 @@ pub const WhileExpr = struct { body: *Node, binding_name: ?[]const u8 = null, // for `while val := expr { ... }` optional binding binding_span: ?Span = null, // span of `binding_name` (set iff `binding_name` is) + /// True when the optional binding was a backtick raw identifier + /// (`` while `s2 := … ``) — exempt from the reserved-type-name check (issue 0089). + binding_is_raw: bool = false, }; pub const ForExpr = struct { @@ -573,8 +596,14 @@ pub const ForExpr = struct { body: *Node, capture_name: []const u8, capture_span: ?Span = null, // span of `capture_name` (null when omitted, e.g. `for 0..N { }`) + /// True when `capture_name` was a backtick raw identifier + /// (`` for xs: (`s2) ``) — exempt from the reserved-type-name check (issue 0089). + capture_is_raw: bool = false, index_name: ?[]const u8 = null, index_span: ?Span = null, // span of `index_name` (set iff `index_name` is) + /// True when `index_name` was a backtick raw identifier + /// (`` for xs: (x, `s2) ``) — exempt from the reserved-type-name check (issue 0089). + index_is_raw: bool = false, /// Range form `for start..end (i) { }`: `iterable` is the start, `range_end` /// the (exclusive) end. Null for the iterate-a-collection form /// (`for coll : (x) { }`). For the range form `capture_name` is the cursor @@ -663,6 +692,10 @@ pub const ProtocolMethodDecl = struct { params: []const *Node, // type_expr nodes for parameter types (excluding implicit self) param_names: []const []const u8, // parameter names (excluding implicit self) param_name_spans: []const Span = &.{}, // one per `param_names` entry; empty for synthesized methods + /// One per `param_names` entry: true when written as a backtick raw + /// identifier — exempt from the reserved-type-name check (issue 0089). + /// Empty for synthesized methods (treated as all-false). + param_name_is_raw: []const bool = &.{}, return_type: ?*Node, // null = void return default_body: ?*Node, // null = required method, non-null = default implementation }; @@ -689,6 +722,10 @@ pub const ForeignMethodDecl = struct { params: []const *Node, // type_expr nodes — first is `*Self` for instance methods param_names: []const []const u8, param_name_spans: []const Span = &.{}, // one per `param_names` entry; empty for synthesized methods + /// One per `param_names` entry: true when written as a backtick raw + /// identifier — exempt from the reserved-type-name check (issue 0089). + /// Empty for synthesized methods (treated as all-false). + param_name_is_raw: []const bool = &.{}, return_type: ?*Node, // null = void is_static: bool = false, // true for `static name :: ...` jni_descriptor_override: ?[]const u8 = null, // `#jni_method_descriptor("(Sig)Ret")` — JNI runtime only diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a83feb0..56c0b05 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -6621,10 +6621,30 @@ pub const Lowering = struct { // ── Calls ─────────────────────────────────────────────────────── fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref { + var c = c_in; + // A bare reserved-type-name spelling in call position parses as a + // `.type_expr` (e.g. `s2(4)`), but if a function of that name is in + // scope — a backtick-declared sx fn or a `#import c` foreign fn whose C + // name collides with a reserved type spelling — it is a CALL to that + // function. `TypeName(val)` is not a cast (casts are `cast(T, val)`), so + // there is no ambiguity. Rewrite the callee to an identifier so the + // normal call machinery resolves it, symmetric to the bare-value + // reference that already resolves via scope/globals (issue 0089). + if (c.callee.data == .type_expr) { + const tname = c.callee.data.type_expr.name; + const is_fn = self.program_index.fn_ast_map.contains(tname) or + (if (self.scope) |scope| scope.lookupFn(tname) != null else false); + if (is_fn) { + const id_node = self.alloc.create(Node) catch unreachable; + id_node.* = .{ .span = c.callee.span, .data = .{ .identifier = .{ .name = tname, .is_raw = true } } }; + const rewritten = self.alloc.create(ast.Call) catch unreachable; + rewritten.* = .{ .callee = id_node, .args = c.args }; + c = rewritten; + } + } // Expand default parameter values for bare identifier callees: // when the caller omits trailing positional args, fill them in // from the callee's `param: T = expr` declarations. - var c = c_in; if (self.expandCallDefaults(c)) |expanded| c = expanded; // Check reflection builtins first (before lowering args — some args are type names, not values) if (c.callee.data == .identifier) { diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 03814df..33bee66 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -121,7 +121,9 @@ pub const UnknownTypeChecker = struct { if (vd.value) |v| self.checkBindingNames(v); }, .destructure_decl => |dd| { - for (dd.names, dd.name_spans) |n, sp| self.checkBindingName(n, sp); + for (dd.names, dd.name_spans, dd.name_is_raw) |n, sp, raw| { + if (!raw) self.checkBindingName(n, sp); + } self.checkBindingNames(dd.value); }, .fn_decl => |fd| { @@ -137,19 +139,25 @@ pub const UnknownTypeChecker = struct { if (p.default_expr) |de| self.checkBindingNames(de); }, .if_expr => |ie| { - if (ie.binding_name) |bn| self.checkBindingName(bn, ie.binding_span); + if (ie.binding_name) |bn| { + if (!ie.binding_is_raw) self.checkBindingName(bn, ie.binding_span); + } 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| self.checkBindingName(bn, we.binding_span); + if (we.binding_name) |bn| { + if (!we.binding_is_raw) self.checkBindingName(bn, we.binding_span); + } self.checkBindingNames(we.condition); self.checkBindingNames(we.body); }, .for_expr => |fe| { - if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span); - if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span); + 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); + } self.checkBindingNames(fe.iterable); if (fe.range_end) |re| self.checkBindingNames(re); self.checkBindingNames(fe.body); @@ -157,23 +165,31 @@ pub const UnknownTypeChecker = struct { .match_expr => |me| { self.checkBindingNames(me.subject); for (me.arms) |arm| { - if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span); + if (arm.capture) |cap| { + if (!arm.capture_is_raw) self.checkBindingName(cap, arm.capture_span); + } if (arm.pattern) |p| self.checkBindingNames(p); self.checkBindingNames(arm.body); } }, .match_arm => |arm| { - if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span); + if (arm.capture) |cap| { + if (!arm.capture_is_raw) self.checkBindingName(cap, arm.capture_span); + } if (arm.pattern) |p| self.checkBindingNames(p); self.checkBindingNames(arm.body); }, .catch_expr => |ce| { - if (ce.binding) |b| self.checkBindingName(b, ce.binding_span); + if (ce.binding) |b| { + if (!ce.binding_is_raw) self.checkBindingName(b, ce.binding_span); + } self.checkBindingNames(ce.operand); self.checkBindingNames(ce.body); }, .onfail_stmt => |os| { - if (os.binding) |b| self.checkBindingName(b, os.binding_span); + if (os.binding) |b| { + if (!os.binding_is_raw) self.checkBindingName(b, os.binding_span); + } self.checkBindingNames(os.body); }, // impl / protocol-default / foreign-class method bodies: each @@ -183,13 +199,19 @@ pub const UnknownTypeChecker = struct { .impl_block => |ib| for (ib.methods) |m| self.checkBindingNames(m), .protocol_decl => |pd| for (pd.methods) |m| { if (m.default_body) |body| { - for (m.param_names, m.param_name_spans) |pn, sp| self.checkBindingName(pn, sp); + 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); + } self.checkBindingNames(body); } }, .foreign_class_decl => |fcd| for (fcd.members) |member| switch (member) { .method => |m| if (m.body) |body| { - for (m.param_names, m.param_name_spans) |pn, sp| self.checkBindingName(pn, sp); + 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); + } self.checkBindingNames(body); }, .field, .extends, .implements => {}, diff --git a/src/parser.zig b/src/parser.zig index a18a7e9..ef4d279 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -629,6 +629,12 @@ 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). + 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)}); + } var name = self.tokenSlice(self.current); self.advance(); @@ -1186,6 +1192,7 @@ pub const Parser = struct { var param_types = std.ArrayList(*Node).empty; var param_names = std.ArrayList([]const u8).empty; var param_name_spans = std.ArrayList(ast.Span).empty; + var param_name_is_raw = std.ArrayList(bool).empty; while (self.current.tag != .r_paren and self.current.tag != .eof) { if (param_types.items.len > 0) { @@ -1198,6 +1205,7 @@ pub const Parser = struct { } const pname = self.tokenSlice(self.current); try param_name_spans.append(self.allocator, .{ .start = self.current.loc.start, .end = self.current.loc.end }); + try param_name_is_raw.append(self.allocator, self.current.is_raw); self.advance(); try self.expect(.colon); const ptype = try self.parseTypeExpr(); @@ -1226,6 +1234,7 @@ pub const Parser = struct { .params = try param_types.toOwnedSlice(self.allocator), .param_names = try param_names.toOwnedSlice(self.allocator), .param_name_spans = try param_name_spans.toOwnedSlice(self.allocator), + .param_name_is_raw = try param_name_is_raw.toOwnedSlice(self.allocator), .return_type = return_type, .default_body = default_body, }); @@ -1454,6 +1463,7 @@ pub const Parser = struct { var param_types = std.ArrayList(*Node).empty; var param_names = std.ArrayList([]const u8).empty; var param_name_spans = std.ArrayList(ast.Span).empty; + var param_name_is_raw = std.ArrayList(bool).empty; while (self.current.tag != .r_paren and self.current.tag != .eof) { if (param_types.items.len > 0) { try self.expect(.comma); @@ -1464,6 +1474,7 @@ pub const Parser = struct { } const pname = self.tokenSlice(self.current); try param_name_spans.append(self.allocator, .{ .start = self.current.loc.start, .end = self.current.loc.end }); + try param_name_is_raw.append(self.allocator, self.current.is_raw); self.advance(); try self.expect(.colon); const ptype = try self.parseTypeExpr(); @@ -1546,6 +1557,7 @@ pub const Parser = struct { .params = try param_types.toOwnedSlice(self.allocator), .param_names = try param_names.toOwnedSlice(self.allocator), .param_name_spans = try param_name_spans.toOwnedSlice(self.allocator), + .param_name_is_raw = try param_name_is_raw.toOwnedSlice(self.allocator), .return_type = return_type, .is_static = is_static, .jni_descriptor_override = desc_override, @@ -2046,7 +2058,7 @@ pub const Parser = struct { // Multi-target assignment: ident, expr, ... = expr, expr, ...; if (self.current.tag == .comma) { - const first_target = try self.createNode(start, .{ .identifier = .{ .name = name } }); + const first_target = try self.createNode(start, .{ .identifier = .{ .name = name, .is_raw = name_is_raw } }); return try self.parseMultiAssign(first_target, start); } @@ -2056,7 +2068,7 @@ pub const Parser = struct { self.advance(); const value = try self.parseExpr(); try self.expect(.semicolon); - const target = try self.createNode(start, .{ .identifier = .{ .name = name } }); + const target = try self.createNode(start, .{ .identifier = .{ .name = name, .is_raw = name_is_raw } }); return try self.createNode(start, .{ .assignment = .{ .target = target, .op = op, .value = value } }); } @@ -2123,9 +2135,11 @@ pub const Parser = struct { self.advance(); var binding: ?[]const u8 = null; var binding_span: ?ast.Span = null; + var binding_is_raw = false; if (self.current.tag == .identifier and self.peekNext() == .l_brace) { binding = self.tokenSlice(self.current); binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; + binding_is_raw = self.current.is_raw; self.advance(); } const saved_onfail = self.in_onfail_body; @@ -2138,7 +2152,7 @@ pub const Parser = struct { try self.expect(.semicolon); break :blk e; }; - return try self.createNode(start, .{ .onfail_stmt = .{ .binding = binding, .binding_span = binding_span, .body = body } }); + return try self.createNode(start, .{ .onfail_stmt = .{ .binding = binding, .binding_span = binding_span, .binding_is_raw = binding_is_raw, .body = body } }); } // Break statement: break; @@ -2570,9 +2584,11 @@ pub const Parser = struct { self.advance(); // consume 'catch' var binding: ?[]const u8 = null; var binding_span: ?ast.Span = null; + var binding_is_raw = false; if (self.current.tag == .identifier) { binding = self.tokenSlice(self.current); binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; + binding_is_raw = self.current.is_raw; self.advance(); } var is_match_body = false; @@ -2582,7 +2598,7 @@ pub const Parser = struct { const m_start = self.current.loc.start; self.advance(); // consume '==' is_match_body = true; - const subject = try self.createNode(m_start, .{ .identifier = .{ .name = binding.? } }); + const subject = try self.createNode(m_start, .{ .identifier = .{ .name = binding.?, .is_raw = binding_is_raw } }); break :blk try self.parseMatchBody(subject, m_start); } else if (binding != null) try self.parseExpr() @@ -2592,6 +2608,7 @@ pub const Parser = struct { .operand = expr, .binding = binding, .binding_span = binding_span, + .binding_is_raw = binding_is_raw, .body = body, .is_match_body = is_match_body, } }); @@ -2690,16 +2707,17 @@ pub const Parser = struct { }, .identifier => { const name = self.tokenSlice(self.current); + const is_raw = self.current.is_raw; // A backtick raw identifier (`` `s2 ``) is NEVER type-classified — // it is always a value identifier, bypassing the reserved-type-name // rule (issue 0089). Only a bare spelling is checked for a type name // (e.g. s32, u8, s128). - if (!self.current.is_raw and Type.fromName(name) != null) { + if (!is_raw and Type.fromName(name) != null) { self.advance(); return try self.createNode(start, .{ .type_expr = .{ .name = name } }); } self.advance(); - return try self.createNode(start, .{ .identifier = .{ .name = name } }); + return try self.createNode(start, .{ .identifier = .{ .name = name, .is_raw = is_raw } }); }, .kw_closure, .kw_protocol, .kw_impl, .kw_ufcs => { // Contextual keywords used as identifiers in expressions @@ -2943,6 +2961,7 @@ pub const Parser = struct { if (self.current.tag == .identifier and self.peekNext() == .colon_equal) { const binding_name = self.tokenSlice(self.current); const binding_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end }; + const binding_is_raw = self.current.is_raw; self.advance(); // skip identifier self.advance(); // skip := const source_expr = try self.parseExpr(); @@ -2963,6 +2982,7 @@ pub const Parser = struct { .is_inline = false, .binding_name = binding_name, .binding_span = binding_span, + .binding_is_raw = binding_is_raw, } }); } @@ -3065,6 +3085,7 @@ pub const Parser = struct { if (self.current.tag == .identifier and self.peekNext() == .colon_equal) { const binding_name = self.tokenSlice(self.current); const binding_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end }; + const binding_is_raw = self.current.is_raw; self.advance(); // skip identifier self.advance(); // skip := const source_expr = try self.parseExpr(); @@ -3074,6 +3095,7 @@ pub const Parser = struct { .body = body, .binding_name = binding_name, .binding_span = binding_span, + .binding_is_raw = binding_is_raw, } }); } @@ -3128,8 +3150,10 @@ pub const Parser = struct { var capture_name: []const u8 = ""; var capture_span: ?ast.Span = null; + var capture_is_raw = false; var index_name: ?[]const u8 = null; var index_span: ?ast.Span = null; + var index_is_raw = false; var capture_by_ref = false; if (range_end != null) { @@ -3142,6 +3166,7 @@ pub const Parser = struct { if (self.current.tag != .identifier) return self.fail("expected cursor variable name"); capture_name = self.tokenSlice(self.current); capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; + capture_is_raw = self.current.is_raw; self.advance(); try self.expect(.r_paren); } @@ -3157,12 +3182,14 @@ pub const Parser = struct { if (self.current.tag != .identifier) return self.fail("expected capture variable name"); capture_name = self.tokenSlice(self.current); capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; + capture_is_raw = self.current.is_raw; self.advance(); if (self.current.tag == .comma) { self.advance(); if (self.current.tag != .identifier) return self.fail("expected index variable name"); index_name = self.tokenSlice(self.current); index_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; + index_is_raw = self.current.is_raw; self.advance(); } try self.expect(.r_paren); @@ -3175,8 +3202,10 @@ pub const Parser = struct { .body = body, .capture_name = capture_name, .capture_span = capture_span, + .capture_is_raw = capture_is_raw, .index_name = index_name, .index_span = index_span, + .index_is_raw = index_is_raw, .range_end = range_end, .capture_by_ref = capture_by_ref, } }); @@ -3202,10 +3231,12 @@ pub const Parser = struct { // arm body (an expression) and is left for the body parse below. var capture: ?[]const u8 = null; var capture_span: ?ast.Span = null; + var capture_is_raw = false; if (self.current.tag == .l_paren and self.isLoneIdentParen()) { self.advance(); // '(' capture = self.tokenSlice(self.current); capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; + capture_is_raw = self.current.is_raw; self.advance(); // ident try self.expect(.r_paren); } @@ -3214,7 +3245,7 @@ pub const Parser = struct { self.advance(); try self.expect(.semicolon); const body = try self.createNode(arm_start, .{ .block = .{ .stmts = &.{} } }); - try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = true, .capture = capture, .capture_span = capture_span }); + try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = true, .capture = capture, .capture_span = capture_span, .capture_is_raw = capture_is_raw }); } else if (self.current.tag == .fat_arrow) { // Short form: (ident) => expr; self.advance(); @@ -3224,7 +3255,7 @@ pub const Parser = struct { // `;` is an arm terminator, not a value-discard — match arms are // exempt from the block trailing-`;` rule). const body = try self.createNode(arm_start, .{ .block = .{ .stmts = try self.allocator.dupe(*Node, &.{expr}), .produces_value = true } }); - try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture, .capture_span = capture_span }); + try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture, .capture_span = capture_span, .capture_is_raw = capture_is_raw }); } else { const stmts_start = self.current.loc.start; var stmts = std.ArrayList(*Node).empty; @@ -3235,7 +3266,7 @@ pub const Parser = struct { // yields its last statement's value — which, for a braced-block // arm body, still respects that inner block's own flag. const body = try self.createNode(stmts_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator), .produces_value = true } }); - try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture, .capture_span = capture_span }); + try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture, .capture_span = capture_span, .capture_is_raw = capture_is_raw }); } } // Optional else arm (default) @@ -3597,18 +3628,21 @@ pub const Parser = struct { // All targets must be plain identifiers var names = std.ArrayList([]const u8).empty; var name_spans = std.ArrayList(ast.Span).empty; + var name_is_raw = std.ArrayList(bool).empty; for (targets.items) |target| { if (target.data != .identifier) { return self.fail("destructuring targets must be identifiers"); } try names.append(self.allocator, target.data.identifier.name); try name_spans.append(self.allocator, target.span); + try name_is_raw.append(self.allocator, target.data.identifier.is_raw); } const value = try self.parseExpr(); try self.expectSemicolonAfter(value); return try self.createNode(start, .{ .destructure_decl = .{ .names = try names.toOwnedSlice(self.allocator), .name_spans = try name_spans.toOwnedSlice(self.allocator), + .name_is_raw = try name_is_raw.toOwnedSlice(self.allocator), .value = value, } }); }