feat(lang): backtick raw-identifier escape + #import c foreign-name exemption [F0.6]

Reserved type-name spellings (s1, s2, u8, …) can now be used as value
identifiers two ways, resolving issue 0089:

1. Backtick raw identifier: a leading backtick (`s2) lexes to an
   .identifier token carrying a new Token.is_raw flag, with the backtick
   excluded from the text. A raw identifier is never type-classified — the
   parser skips Type.fromName for it — so it is always a value identifier.
   The flag threads to VarDecl.is_raw / Param.is_raw at binding sites, and
   the reserved-type-name check (UnknownTypeChecker) skips raw bindings.
   Because the token tag stays .identifier, the escape works in every
   position (local, global, param, field, fn name, struct member, later
   reference) with no per-site parser change.

2. #import c exemption: c_import.zig synthesizes foreign decls with
   Param.is_raw = true, so generated C param names that collide with
   reserved type names (s1, s2) import unedited.

A bare reserved-name binding in sx still errors (issue 0076 preserved):
the is_raw-gated skip only fires for backtick / foreign names, and a raw
binding's address-of / autoref lowering stays correct because every
occurrence is an .identifier, never a .type_expr.

Tests: examples/0151 (backtick, every position),
examples/1220 (foreign exemption, compiled+run), lexer unit tests.
1119 (bare-binding rejection) stays green. specs.md + readme.md updated.
This commit is contained in:
agra
2026-06-04 17:40:42 +03:00
parent 7911494809
commit 0dbdc530ba
19 changed files with 317 additions and 14 deletions

View File

@@ -0,0 +1,91 @@
# 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 new `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.
> 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.
>
> 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`.
>
> 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.