Files
sx/issues/0089-backtick-raw-identifier.md
agra c0e1a5db82 feat(lang): reserved-name check covers :: const/fn/type decls + scope call rewrite to raw provenance [F0.6]
A bare reserved-type-name `::` declaration was silently accepted, and the
attempt-2 lowerCall rewrite then made a bare `s2 :: (…) {…}` function callable —
bypassing the backtick rule for handwritten sx. The reserved-name binding check
covered `:=` / typed-local / param / captures but NOT the `::` declaration form.

- ast: `ConstDecl`/`FnDecl` carry `is_raw` + `name_span` threaded from the parser
  (parseConstBinding / parseFnDecl, all call sites incl. struct/impl methods).
- semantic_diagnostics: reject a bare reserved spelling at EVERY declaration-name
  site — const, function (incl. struct/impl methods), struct/enum/union/error-set,
  protocol, foreign-class, ufcs alias, namespaced/library/c-import name. Backtick
  (`is_raw`) and the compiler's `#builtin` definition (`string :: []u8 #builtin`)
  are the only exemptions; a value whose node is itself a named decl defers to
  that node's own check.
- c_import: synthesized foreign fn_decls are `is_raw = true`, so a C function
  whose own name collides with a reserved spelling (`int s2(int);`) imports and
  bare-calls unedited.
- lower: scope the `.type_expr`→`.identifier` call rewrite to a callee FnDecl of
  RAW provenance (`is_raw`) — only a backtick / `#import c` foreign fn can carry a
  reserved-name spelling, so a non-raw match never gets rewritten.
- examples: 0153 (positive — backtick `::` const + fn, bare + tick call), 1140
  (negative — bare `::` const + fn rejected).
- docs: specs.md + readme.md state the backtick is required at every binding site
  including `::` const / function / type declarations; issue 0089 banner updated.
2026-06-04 19:16:37 +03:00

126 lines
6.6 KiB
Markdown

# 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:
>
> 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).
>
> 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.
> 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 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.
>
> 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`.
>
> The original report is preserved below.
---
## Symptom
Importing non-sx source whose names collide with sx reserved type names is
rejected. `library/modules/stb_truetype.sx` is a `#import c { ... }` block over a
vendored C header (`vendors/stb_truetype/stb_truetype.h`); C identifiers `s1`,
`s2` (which collide with sx's signed-int type keywords `s1`..`sN`) produce:
```
error: 's1' is a reserved type name and cannot be used as an identifier
error: 's2' is a reserved type name and cannot be used as an identifier
```
The user cannot hand-edit these — they are generated from the vendored C header.
Separately, sx-authored code has NO way to deliberately use a reserved-name-spelled
identifier even when it wants to.
## Root cause
The parser classifies any reserved-type-name spelling (`s2`, `u8`, `f64`, …) as a
`.type_expr` via `name_class.Type.fromName`, never as an `.identifier`. The F0.1 /
issue-0076 fix added `UnknownTypeChecker.checkBindingName`
(`src/ir/semantic_diagnostics.zig`) to reject a value binding / param spelled as
a reserved type name (the `.type_expr`-vs-`.identifier` mismatch otherwise breaks
address-of / autoref lowering). F0.1 deliberately extended this check to imported
declarations — which is what now fires on the C-imported `s1`/`s2`.
## Desired behaviour (Agra ruling)
External / imported source does NOT need to conform to sx naming standards. Two
mechanisms:
1. **Auto-exempt imports.** `#import c` (and other foreign) declarations are
treated as RAW identifiers: foreign names are never type-classified and never
reserved-checked, so generated bindings "just work" with zero user edits.
2. **Backtick raw-identifier for sx code.** A leading backtick makes the following
identifier raw — an identifier that is NEVER type-classified, so it bypasses the
reserved-name rule:
```sx
`s2 := 2.5; // OK — identifier "s2", distinct from the s2 signed-int type
s2 := 2.5; // ERROR — bare s2 is still the reserved type name
```
Prefix form (single leading backtick on the identifier). The raw identifier's
TEXT is `s2` (the backtick is not part of the name). A bare `s2` used as a TYPE
remains the signed-int type.
## Reproduction
sx-side (minimal):
```sx
#import "modules/std.sx";
main :: () {
`s2 := 2.5; // must compile: identifier s2 = 2.5
print("{}\n", `s2); // 2.5
}
```
Import-side: a `#import c` block over a header declaring `int s1, s2;` (or
`stb_truetype.sx`) must NOT emit the reserved-type-name error.