AGRA ruling (attempt 4): `` `name `` is THE LITERAL identifier `name`, usable in EVERY position — the backtick only means "treat this token as a plain identifier, never the reserved keyword/type", and is never part of the name's text. - Raw in TYPE position is now VALID (reverses attempt-2 "raw is not a type"): `parseTypeExpr` emits a raw `type_expr`; `TypeResolver.resolveNamed` gains a `skip_builtin` flag (threaded from `te.is_raw` via lower.zig + type_bridge) so a `` `s2 `` reference resolves to a `` `s2 ``-declared type (struct/enum/union/alias), else a normal "unknown type 's2'" error (reportIfUnknownType skips the builtin exemption when raw). Bare `s2` in type position stays the builtin int. - Every declaration-name site is is_raw-exemptible: `is_raw` added to TypeExpr + StructDecl/EnumDecl/UnionDecl/ErrorSetDecl/ProtocolDecl/ForeignClassDecl/UfcsAlias/ NamespaceDecl/ImportDecl/CImportDecl/LibraryDecl; parser threads name_is_raw to every decl parse fn; namespace imports carry it through imports.addNamespace. Typed-const path (`` `s2 : s64 : 5 ``) now threads name_span+is_raw (fixes the 1:1-caret bug). - Check<->exemption made structurally symmetric: checkBindingName/checkDeclName take is_raw as a REQUIRED argument and skip inside the check, so no call site can validate a name without honoring the exemption (the desync cause of prior rounds). - Bare reserved-name declarations of every kind still error (0076 preserved); `#import c` foreign names stay auto-raw + bare-callable. specs.md + readme.md updated to the universal model. issue 0089 RESOLVED banner rewritten. Examples: replace 1139 (raw-not-a-type) with 0154 (raw type reference); add 0155 (typed const + union tag) and 1141 (bare type-decl negatives). Gate: zig build + zig build test + run_examples (426 passed, 0 failed).
132 lines
7.0 KiB
Markdown
132 lines
7.0 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; 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]). 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`.
|
|
>
|
|
> - **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` (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.
|
|
>
|
|
> **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).
|
|
>
|
|
> **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.
|
|
|
|
---
|
|
|
|
## 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.
|