Files
sx/issues/0089-backtick-raw-identifier.md
agra 0dbdc530ba 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.
2026-06-04 17:40:42 +03:00

4.2 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 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:

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