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

6.6 KiB

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] parseTypeExpratom). A reserved-spelled FUNCTION (backtick-declared or#import cforeign) is bare-callable:lowerCallrewrites a.type_exprcallee 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 calleeFnDecl's is_rawflag (F0.6 attempt-3), so it only ever fires for a backtick /#import cforeign fn (the decl check guarantees no bare reserved-name fn exists), sos2(4) resolves to the function (TypeName(val)is not a cast). A later BARE reference in value position resolves to the binding; a bares2` 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:

    `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):

#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.