feat(ffi-linkage): fn-path accepts postfix extern/export + lib/name fields (Phase 1.0a)
parseFnDecl now calls parseOptionalExternExport() after the callconv
slot and stores the modifier on FnDecl.extern_export. For 'extern' the
body is ';' (an empty-block placeholder — the modifier carries the
linkage, no *_expr node, per the naming constraint). Both fn-decl
lookahead predicates (isFunctionDef, hasFnBodyAfterArrow) now treat
kw_extern/kw_export as fn-body markers beside kw_callconv, so
'(...) -> R extern;' is recognized as a fn def rather than a fn-type
const.
Per user feedback, decision 4 ("library separate") is REVISED: extern
carries an optional LIB + "csym" axis mirroring '#foreign LIB "csym"',
so it is a true #foreign superset (Gate A->B requirement — the Part B
migration of 466 #foreign uses across 6 libs must preserve each
symbol's library). Added FnDecl.extern_lib/extern_name and
VarDecl.extern_lib (beside is_extern/extern_name).
All unconsumed by lowering: extern parses, but a fn still errors at
sema (body produces no value). Suite green (443 unit / 633 corpus).
lock commit.
This commit is contained in:
@@ -5,33 +5,40 @@ Companion to `current/PLAN-EXTERN-EXPORT.md` — one merged plan: **Part A** add
|
|||||||
every commit, one step at a time per the cadence rule.
|
every commit, one step at a time per the cadence rule.
|
||||||
|
|
||||||
## Last completed step
|
## Last completed step
|
||||||
**Phase 0.1** (lock) — added `ast.ExternExportModifier = enum { none, extern_,
|
**Phase 1.0a** (lock, green — parse half of 1.0) — the fn-decl path now ACCEPTS
|
||||||
export_ }` (beside `CallingConvention`), `FnDecl.extern_export` (default `.none`),
|
postfix `extern`/`export`. `parseFnDecl` calls `parseOptionalExternExport()` after the
|
||||||
`VarDecl.is_extern`/`extern_name` (defaults absent), and
|
callconv slot and stores it on `FnDecl.extern_export`; for `extern` the body is `;`
|
||||||
`parser.parseOptionalExternExport()` (mirrors `parseOptionalCallConv`, just below it
|
(an empty-block placeholder — modifier carries the linkage, no `*_expr` node). Both
|
||||||
at `parser.zig:~3683`). **Helper + fields defined but NOT consumed by any decl path**
|
lookahead predicates (`isFunctionDef` + `hasFnBodyAfterArrow`) now treat
|
||||||
— no user-facing behavior change, corpus diff empty. Two inline parser unit tests
|
`kw_extern`/`kw_export` as fn-body markers (beside `kw_callconv`) so `(...) -> R
|
||||||
(`parseOptionalExternExport recognizes linkage keywords`, `extern/export AST fields
|
extern;` is recognized as a fn def, not a fn-type const. **Per user feedback (decision
|
||||||
default to absent`). Suite green (443 unit [+2] / 633 corpus, 0 fail).
|
4 REVISED):** added `extern_lib`+`extern_name` to `FnDecl` and `extern_lib` to
|
||||||
|
`VarDecl` (next to `is_extern`/`extern_name`) — the optional `LIB`+`"csym"` axis
|
||||||
|
mirroring `#foreign LIB "csym"`, so `extern` is a true `#foreign` superset (Gate A→B).
|
||||||
|
All unconsumed by lowering — `extern` parses but a fn still errors at sema
|
||||||
|
(`body produces no value`). Suite green (443 unit / 633 corpus, 0 fail).
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
Syntax decided + ratified: bare `extern`/`export`, postfix in the `callconv(.c)`
|
Syntax: bare `extern`/`export`, postfix after `callconv(.c)`, `extern ⇒ callconv(.c)`.
|
||||||
slot, `extern ⇒ callconv(.c)`, library separate. Touch-points mapped — token
|
**Decision 4 revised** (user 2026-06-14): `extern` carries an optional `LIB`+`"csym"`
|
||||||
`token.zig:45,282`; parser `1950,3669,316,425,1305`; lowering
|
axis (`extern_lib`/`extern_name`) like `#foreign`; the `#library` decl + build-flag
|
||||||
`decl.zig:1123,387,2110,2382,2514`; IR/emit already capable (no codegen change).
|
linking stays separate. Touch-points: token `token.zig:45,282`; parser
|
||||||
Export gap = 4 lowering conditions. Part B `foreign` footprint to purge: 643 lines /
|
`1950,3669,316,425,1305`; lowering `decl.zig:1123,387,2110,2382,2514`; IR/emit already
|
||||||
~57 identifiers in `src/` + 28 doc lines. End-state invariant: **zero `foreign`** in
|
capable (no codegen change). Part B `foreign` footprint to purge: 643 lines / ~57
|
||||||
the live tree (Phase 9.4 gate). **Phase 0 done**: tokens (0.0) + AST/parser plumbing
|
identifiers in `src/` + 28 doc lines. End-state invariant: **zero `foreign`** (Phase
|
||||||
(0.1) exist, unconsumed. Phase 1 wires `extern` into the fn/global decl paths.
|
9.4 gate). **Done**: tokens (0.0) + AST/parser plumbing (0.1) + fn-path extern parsing
|
||||||
|
+ lib/name fields (1.0a), all unconsumed. Lowering not yet wired.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
**Phase 1.0** (xfail) — accept postfix `extern` after the callconv slot in the
|
**Phase 1.0b** (xfail — example half of 1.0) — add `examples/12xx-ffi-extern-fn.sx`
|
||||||
fn-decl path (`parser.zig:1950`: call `parseOptionalExternExport()`, store on
|
that extern-binds a libc symbol (`abs`); expected files capture the SUCCESS output, so
|
||||||
`FnDecl.extern_export`); add `examples/12xx-ffi-extern-fn.sx` that extern-binds a libc
|
the example is **red** now (parses, then errors at sema since lowering is unwired).
|
||||||
symbol — **red** (parses, but lowering not wired yet). Then 1.1 green (lower via
|
Then **1.1** (green): in `decl.zig`, when `extern_export == .extern_`, route the fn
|
||||||
`declareExtern`: `is_extern`, `.external`, `callconv(.c)`, no ctx — anchors
|
through `declareExtern` (`is_extern`, `.external`, `callconv(.c)`, no ctx — anchors
|
||||||
`decl.zig:1123,387,2110,2113`), 1.2 green (`extern "csym"` rename + extern-global
|
`decl.zig:1123,387,2110,2113`) instead of lowering the placeholder body → example
|
||||||
`g : T extern;`, `parser.zig:425`). Stop at end of Phase 1.
|
green. Then **1.2** (green): `extern LIB "csym"` rename (consume `extern_lib`/
|
||||||
|
`extern_name`) + extern-global `g : T extern;` (`parser.zig:425`). Stop at end of
|
||||||
|
Phase 1.
|
||||||
|
|
||||||
## Open decisions
|
## Open decisions
|
||||||
Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm
|
Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ superset of `#foreign`, and Part A isn't "done" until Part B reaches the invaria
|
|||||||
|
|
||||||
**Decided syntax**
|
**Decided syntax**
|
||||||
```sx
|
```sx
|
||||||
name :: (sig) -> Ret [callconv(.x)] [extern | export] [;|{…}]; // functions
|
name :: (sig) -> Ret [callconv(.x)] [extern | export] [LIB] ["csym"] [;|{…}]; // functions
|
||||||
Name :: #objc_class("X") [extern | export] { … }; // aggregates (mirrors `struct #compiler`)
|
Name :: #objc_class("X") [extern | export] { … }; // aggregates (mirrors `struct #compiler`)
|
||||||
g : Type extern ["csym"]; // extern global
|
g : Type extern [LIB] ["csym"]; // extern global
|
||||||
```
|
```
|
||||||
- `extern` = import (no body, external linkage, C ABI, no sx ctx) — `#foreign`'s role.
|
- `extern` = import (no body, external linkage, C ABI, no sx ctx) — `#foreign`'s role.
|
||||||
- `export` = define **and** expose (body + external linkage + C ABI + no ctx) — **new**.
|
- `export` = define **and** expose (body + external linkage + C ABI + no ctx) — **new**.
|
||||||
- `extern`/`export` imply `callconv(.c)`; write `callconv` only to override.
|
- `extern`/`export` imply `callconv(.c)`; write `callconv` only to override.
|
||||||
- Library stays a separate axis (`#library`/build flags), not folded into `extern`.
|
- Optional `LIB` (a `#library` alias) + `"csym"` rename mirror `#foreign LIB "csym"`,
|
||||||
|
so `extern` is a true `#foreign` **superset** (Gate A→B): carried on
|
||||||
|
`extern_lib`/`extern_name`. The `#library` declaration + build-flag linking
|
||||||
|
mechanism stays a separate axis — `extern` *references* a lib, it doesn't fold
|
||||||
|
in `#library` itself. (Revises the original "library fully separate" decision 4.)
|
||||||
|
|
||||||
> **END-STATE INVARIANT (hard requirement).** After this stream, `foreign` appears
|
> **END-STATE INVARIANT (hard requirement).** After this stream, `foreign` appears
|
||||||
> **nowhere** in the live tree — not the `#foreign` surface, and **not** internal
|
> **nowhere** in the live tree — not the `#foreign` surface, and **not** internal
|
||||||
@@ -29,8 +33,9 @@ g : Type extern ["csym"]; // extern g
|
|||||||
`extern`-named representations only — do **not** reuse or extend
|
`extern`-named representations only — do **not** reuse or extend
|
||||||
`ForeignExpr`/`foreign_expr`/`VarDecl.is_foreign`. Carry extern/export on a new
|
`ForeignExpr`/`foreign_expr`/`VarDecl.is_foreign`. Carry extern/export on a new
|
||||||
`FnDecl.extern_export` modifier with a `;`/`{…}` body (so there is **no** `*_expr`
|
`FnDecl.extern_export` modifier with a `;`/`{…}` body (so there is **no** `*_expr`
|
||||||
node for it); add `VarDecl.is_extern`/`extern_name`. The IR is already extern-named
|
node for it) + `FnDecl.extern_lib`/`extern_name`; add `VarDecl.is_extern`/
|
||||||
(`Function.is_extern`, `Builder.declareExtern`).
|
`extern_lib`/`extern_name`. The IR is already extern-named (`Function.is_extern`,
|
||||||
|
`Builder.declareExtern`).
|
||||||
|
|
||||||
**Key finding (scopes Part A):** the IR + LLVM emit **already support everything** —
|
**Key finding (scopes Part A):** the IR + LLVM emit **already support everything** —
|
||||||
`Function.linkage` (`.external/.internal/.private`), `is_extern`, `call_conv`, and a
|
`Function.linkage` (`.external/.internal/.private`), `is_extern`, `call_conv`, and a
|
||||||
@@ -147,7 +152,11 @@ per-file/subsystem commits — not one sweep.
|
|||||||
## Open decisions
|
## Open decisions
|
||||||
*Part A (ratified — recommendations stand):* 1. bare keywords (not `#extern`).
|
*Part A (ratified — recommendations stand):* 1. bare keywords (not `#extern`).
|
||||||
2. aggregate position postfix (`#objc_class(…) extern`, like `struct #compiler`).
|
2. aggregate position postfix (`#objc_class(…) extern`, like `struct #compiler`).
|
||||||
3. `extern ⇒ callconv(.c)`. 4. library separate.
|
3. `extern ⇒ callconv(.c)`. 4. **REVISED** (user, 2026-06-14): `extern` carries an
|
||||||
|
optional `LIB`+`"csym"` axis (`extern_lib`/`extern_name`), mirroring `#foreign LIB
|
||||||
|
"csym"`, so it's a true `#foreign` superset (Gate A→B). The `#library` declaration +
|
||||||
|
build-flag linking mechanism stays separate — `extern` references a lib, doesn't
|
||||||
|
fold in `#library`. (Was: "library fully separate / not on `extern`".)
|
||||||
*Part B (confirm before Phase 9):* 5. runtime-class rename target — **`Runtime*Class*`**
|
*Part B (confirm before Phase 9):* 5. runtime-class rename target — **`Runtime*Class*`**
|
||||||
(recommended; it's the object-model axis, not linkage) vs `Extern*Class*`.
|
(recommended; it's the object-model axis, not linkage) vs `Extern*Class*`.
|
||||||
6. historical carve-out — keep `issues/*.md` (+ design-doc prose) as provenance,
|
6. historical carve-out — keep `issues/*.md` (+ design-doc prose) as provenance,
|
||||||
|
|||||||
22
src/ast.zig
22
src/ast.zig
@@ -143,6 +143,15 @@ pub const FnDecl = struct {
|
|||||||
/// `callconv(...)` slot. `.none` for an ordinary sx-internal function.
|
/// `callconv(...)` slot. `.none` for an ordinary sx-internal function.
|
||||||
/// Parsed in Phase 0.1; not consumed by the fn-decl path until Phase 1.
|
/// Parsed in Phase 0.1; not consumed by the fn-decl path until Phase 1.
|
||||||
extern_export: ExternExportModifier = .none,
|
extern_export: ExternExportModifier = .none,
|
||||||
|
/// Optional library reference + symbol-name override for an `extern`/`export`
|
||||||
|
/// function, mirroring `#foreign LIB "csym"` (foreign_lib/foreign_name). Both
|
||||||
|
/// optional: `extern` alone resolves the sx name against the default-linked
|
||||||
|
/// libs; `extern LIB` names the source library; `extern "csym"` renames the
|
||||||
|
/// symbol. Required for `extern` to be a behavior-equivalent superset of
|
||||||
|
/// `#foreign` (Gate A→B) — the migration of 466 `#foreign` uses across 6 libs
|
||||||
|
/// must preserve each symbol's library. Parsed/consumed in Phase 1.2.
|
||||||
|
extern_lib: ?[]const u8 = null,
|
||||||
|
extern_name: ?[]const u8 = null,
|
||||||
/// Span of the function's name token, for the reserved-type-name decl
|
/// Span of the function's name token, for the reserved-type-name decl
|
||||||
/// diagnostic. Synthesized decls (e.g. `#import c` foreign
|
/// diagnostic. Synthesized decls (e.g. `#import c` foreign
|
||||||
/// functions, lowering-time objc/protocol method synthesis) leave it zero.
|
/// functions, lowering-time objc/protocol method synthesis) leave it zero.
|
||||||
@@ -355,12 +364,15 @@ pub const VarDecl = struct {
|
|||||||
is_foreign: bool = false,
|
is_foreign: bool = false,
|
||||||
foreign_lib: ?[]const u8 = null,
|
foreign_lib: ?[]const u8 = null,
|
||||||
foreign_name: ?[]const u8 = null,
|
foreign_name: ?[]const u8 = null,
|
||||||
/// `extern`-global form `g : T extern ["csym"];` — a reference to a global
|
/// `extern`-global form `g : T extern [LIB] ["csym"];` — a reference to a
|
||||||
/// defined elsewhere (external linkage, resolved at link time). The new
|
/// global defined elsewhere (external linkage, resolved at link time). The
|
||||||
/// extern-named surface; distinct from the legacy `#foreign` path above.
|
/// new extern-named surface; distinct from the legacy `#foreign` path above.
|
||||||
/// `extern_name` is the optional symbol-name override. Parsed in Phase 0.1;
|
/// `extern_lib` is the optional source-library reference and `extern_name`
|
||||||
/// not consumed by the var-decl path until Phase 1.2.
|
/// the optional symbol-name override (mirroring foreign_lib/foreign_name, so
|
||||||
|
/// `extern` fully supersedes `#foreign`). Parsed in Phase 0.1; not consumed
|
||||||
|
/// by the var-decl path until Phase 1.2.
|
||||||
is_extern: bool = false,
|
is_extern: bool = false,
|
||||||
|
extern_lib: ?[]const u8 = null,
|
||||||
extern_name: ?[]const u8 = null,
|
extern_name: ?[]const u8 = null,
|
||||||
/// True when the binding name was written as a backtick raw identifier
|
/// True when the binding name was written as a backtick raw identifier
|
||||||
/// (`` `i2 := … ``). A raw name is exempt from the reserved-type-name
|
/// (`` `i2 := … ``). A raw name is exempt from the reserved-type-name
|
||||||
|
|||||||
@@ -1949,9 +1949,22 @@ pub const Parser = struct {
|
|||||||
// Optional calling convention: callconv(.c)
|
// Optional calling convention: callconv(.c)
|
||||||
const call_conv = try self.parseOptionalCallConv();
|
const call_conv = try self.parseOptionalCallConv();
|
||||||
|
|
||||||
// Body: block `{ ... }`, arrow `=> expr;`, #builtin, #compiler, or #foreign marker
|
// Optional postfix linkage modifier: `extern` (import) / `export` (define).
|
||||||
|
const extern_export = self.parseOptionalExternExport();
|
||||||
|
|
||||||
|
// Body: block `{ ... }`, arrow `=> expr;`, #builtin, #compiler, or #foreign marker.
|
||||||
|
// An `extern` import has NO body — just `;`. The extern_export modifier
|
||||||
|
// carries the linkage; we synthesize an empty block as the (non-optional)
|
||||||
|
// body placeholder, and lowering routes on the modifier rather than this
|
||||||
|
// block (no `*_expr` node — naming-constraint rule). `export` keeps its
|
||||||
|
// `{ … }` body and flows through the normal chain below.
|
||||||
var is_arrow = false;
|
var is_arrow = false;
|
||||||
const body = if (self.current.tag == .hash_builtin) blk: {
|
const body = if (extern_export == .extern_) blk: {
|
||||||
|
const semi_start = self.current.loc.start;
|
||||||
|
try self.expect(.semicolon);
|
||||||
|
const stmts = try self.allocator.alloc(*Node, 0);
|
||||||
|
break :blk try self.createNode(semi_start, .{ .block = .{ .stmts = stmts, .produces_value = false } });
|
||||||
|
} else if (self.current.tag == .hash_builtin) blk: {
|
||||||
const bi_start = self.current.loc.start;
|
const bi_start = self.current.loc.start;
|
||||||
self.advance();
|
self.advance();
|
||||||
try self.expect(.semicolon);
|
try self.expect(.semicolon);
|
||||||
@@ -2011,6 +2024,7 @@ pub const Parser = struct {
|
|||||||
.type_params = type_params,
|
.type_params = type_params,
|
||||||
.is_arrow = is_arrow,
|
.is_arrow = is_arrow,
|
||||||
.call_conv = call_conv,
|
.call_conv = call_conv,
|
||||||
|
.extern_export = extern_export,
|
||||||
.name_span = name_span,
|
.name_span = name_span,
|
||||||
.is_raw = name_is_raw,
|
.is_raw = name_is_raw,
|
||||||
} });
|
} });
|
||||||
@@ -3609,7 +3623,9 @@ pub const Parser = struct {
|
|||||||
// `(T1, T2) -> R` without a trailing body (`{`, `=>`, or a foreign/
|
// `(T1, T2) -> R` without a trailing body (`{`, `=>`, or a foreign/
|
||||||
// builtin marker) is a function-type literal, not a function def.
|
// builtin marker) is a function-type literal, not a function def.
|
||||||
if (tag == .arrow) return self.hasFnBodyAfterArrow();
|
if (tag == .arrow) return self.hasFnBodyAfterArrow();
|
||||||
return tag == .l_brace or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow or tag == .kw_callconv;
|
// `kw_extern`/`kw_export`: a postfix linkage modifier (e.g. `f :: () extern;`
|
||||||
|
// with no return type) marks a fn decl just like `callconv`.
|
||||||
|
return tag == .l_brace or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow or tag == .kw_callconv or tag == .kw_extern or tag == .kw_export;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasFnBodyAfterArrow(self: *Parser) bool {
|
fn hasFnBodyAfterArrow(self: *Parser) bool {
|
||||||
@@ -3637,6 +3653,9 @@ pub const Parser = struct {
|
|||||||
if (self.current.tag == .l_brace) return true;
|
if (self.current.tag == .l_brace) return true;
|
||||||
if (self.current.tag == .hash_builtin or self.current.tag == .hash_compiler or self.current.tag == .hash_foreign) return true;
|
if (self.current.tag == .hash_builtin or self.current.tag == .hash_compiler or self.current.tag == .hash_foreign) return true;
|
||||||
if (self.current.tag == .kw_callconv) return true;
|
if (self.current.tag == .kw_callconv) return true;
|
||||||
|
// Postfix linkage modifier after the return type: `-> R extern;` /
|
||||||
|
// `-> R export { … }` (and `-> R callconv(.c) extern`). Marks a fn def.
|
||||||
|
if (self.current.tag == .kw_extern or self.current.tag == .kw_export) return true;
|
||||||
// Inside a `struct #compiler` block, a `(...) -> Ret;` ending
|
// Inside a `struct #compiler` block, a `(...) -> Ret;` ending
|
||||||
// with `;` after the return type is a `#compiler` method
|
// with `;` after the return type is a `#compiler` method
|
||||||
// declaration (body implicit). Outside that context, the same
|
// declaration (body implicit). Outside that context, the same
|
||||||
|
|||||||
Reference in New Issue
Block a user