feat(ffi-linkage): lower extern fns as C imports (Phase 1.1)
Route a bare 'extern' fn declare-only, exactly like a lib-less #foreign import. Six edits in decl.zig, each mirroring an existing foreign_expr guard so the empty-block placeholder body is never lowered: 1. funcWantsImplicitCtx: suppress the implicit __sx_ctx for .extern_ 2. declareFunction: add is_extern_decl 3. ...and include it in the C-ABI calling-convention promotion 4. lazyLowerFunction: .extern_ -> declareFunction (declare-only) 5. lowerFunction: .extern_ in the declare-only guard 6. lowerFunctionBodyInto: never promote/lower an extern stub examples/1223 now green: 'extern' abs lowers to 'declare i32 @abs(i32)' (external linkage, C ABI, no ctx param) and the call resolves against the default-linked libc -> abs(-7)=7, abs(42)=42. The 1.0b hand-authored snapshot matched byte-exact (no regen). Suite green (634 corpus / 443 unit). green commit (makes the 1.0b xfail pass; adds no new test).
This commit is contained in:
@@ -5,38 +5,47 @@ 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 1.0b** (xfail — example half of 1.0) — added `examples/1223-ffi-extern-fn.sx`
|
**Phase 1.1** (green) — wired extern fn lowering in `decl.zig`; example **1223 now
|
||||||
(extern-binds libc `abs` via bare `extern`; sx name = C symbol, no rename) +
|
green**, full suite green (634 corpus / 443 unit, 0 fail). A bare `extern` fn lowers
|
||||||
hand-authored `expected/` capturing the SUCCESS output (`abs(-7) = 7` / `abs(42) =
|
exactly like a lib-less `#foreign` import: `declare i32 @abs(i32) #0` — external
|
||||||
42`, exit 0). **RED**: 1223 is the only corpus failure (634 ran, 1 failed) — it parses
|
linkage, C ABI, NO `__sx_ctx` param; calls emit `call i32 @abs(i32 -7)` and resolve
|
||||||
then errors at sema (`body produces no value`) because lowering doesn't route extern
|
against the default-linked libc. Six edits, all routing `extern` declare-only
|
||||||
yet. Phase 1.1 turns it green. (Prior: 1.0a (lock, green) wired fn-path extern
|
(mirroring the `foreign_expr` guards): (1) `funcWantsImplicitCtx` suppresses ctx for
|
||||||
parsing — `parseFnDecl` → `parseOptionalExternExport()` → `FnDecl.extern_export`,
|
`.extern_`; (2) `declareFunction` adds `is_extern_decl` and (3) includes it in the
|
||||||
`;` body = empty-block placeholder; both lookahead predicates accept
|
C-ABI promotion; (4) `lazyLowerFunction` routes `.extern_`→declare-only; (5)
|
||||||
`kw_extern`/`kw_export`; per user feedback added `FnDecl.extern_lib`/`extern_name` +
|
`lowerFunction` declare-only guard; (6) `lowerFunctionBodyInto` never promotes/lowers
|
||||||
`VarDecl.extern_lib`, decision 4 REVISED.)
|
an extern stub. Hand-authored 1.0b snapshot matched byte-exact — no regen needed. No
|
||||||
|
`.ir` snapshot added (the trivial `declare i32 @abs(i32)` doesn't warrant a 1000-line
|
||||||
|
full-prelude dump; behavioral `.stdout` already catches ctx/linkage regressions).
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
Syntax: bare `extern`/`export`, postfix after `callconv(.c)`, `extern ⇒ callconv(.c)`.
|
Syntax: bare `extern`/`export`, postfix after `callconv(.c)`, `extern ⇒ callconv(.c)`.
|
||||||
**Decision 4 revised** (user 2026-06-14): `extern` carries an optional `LIB`+`"csym"`
|
**Decision 4 revised** (user 2026-06-14): `extern` carries an optional `LIB`+`"csym"`
|
||||||
axis (`extern_lib`/`extern_name`) like `#foreign`; the `#library` decl + build-flag
|
axis (`extern_lib`/`extern_name`) like `#foreign`; the `#library` decl + build-flag
|
||||||
linking stays separate. Touch-points: token `token.zig:45,282`; parser
|
linking stays separate. **`extern` FUNCTIONS WORK** (import; bare form, no rename) —
|
||||||
`1950,3669,316,425,1305`; lowering `decl.zig:1123,387,2110,2382,2514`; IR/emit already
|
parse + lower complete, behavior-equivalent to a lib-less `#foreign` fn. Still TODO in
|
||||||
capable (no codegen change). Part B `foreign` footprint to purge: 643 lines / ~57
|
Phase 1: the `extern LIB "csym"` lib/rename axis (fields exist, unconsumed) and the
|
||||||
identifiers in `src/` + 28 doc lines. End-state invariant: **zero `foreign`** (Phase
|
extern-global form. `export` not started (Phase 2). Part B `foreign` footprint to
|
||||||
9.4 gate). **Done**: tokens (0.0) + AST/parser plumbing (0.1) + fn-path extern parsing
|
purge: 643 lines / ~57 identifiers in `src/` + 28 doc lines. End-state invariant:
|
||||||
+ lib/name fields (1.0a), all unconsumed. Lowering not yet wired.
|
**zero `foreign`** (Phase 9.4 gate). **Done**: 0.0 tokens, 0.1 AST/parser plumbing,
|
||||||
|
1.0a fn-path parsing + lib/name fields, 1.0b xfail example, 1.1 fn lowering (green).
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
**Phase 1.1** (green, turns 1223 green) — in `decl.zig`, when
|
**Phase 1.2** (green) — two parts, each its own xfail→green or behavior-lock:
|
||||||
`fn_decl.extern_export == .extern_`, route the fn through `declareExtern` (`is_extern`,
|
1. **`extern LIB "csym"` rename for fns** — extend `parseOptionalExternExport()` (or
|
||||||
`.external` linkage, `callconv(.c)`, no implicit ctx — anchors `decl.zig:1123,387,
|
`parseFnDecl`) to parse the optional `LIB` ident + `"csym"` string after the
|
||||||
2110,2113`) instead of lowering the empty-block placeholder body. Treat the bare
|
keyword into `FnDecl.extern_lib`/`extern_name`; consume them in `declareFunction`
|
||||||
`extern` (no `extern_lib`/`extern_name` yet) like a lib-less `#foreign` import — the
|
(mirror the `#foreign` c_name block at `decl.zig:~2119`: declare under the C name,
|
||||||
sx name IS the C symbol, resolves against the default-linked libc. Run, then
|
map sx→C in `foreign_name_map`/dedupe). New example renaming a libc symbol (e.g.
|
||||||
`-Dupdate-goldens` to finalize 1223's snapshot byte-exact; review diff. Then **1.2**
|
`c_abs :: (n: i32) -> i32 extern "abs";`).
|
||||||
(green): consume `extern LIB "csym"` rename (`extern_lib`/`extern_name`) + extern-global
|
2. **extern-global `g : T extern [LIB] ["csym"];`** — parse path at `parser.zig:425`
|
||||||
`g : T extern;` (`parser.zig:425`). Stop at end of Phase 1.
|
(the var-decl with type annotation): accept postfix `extern` → set
|
||||||
|
`VarDecl.is_extern`/`extern_lib`/`extern_name`; lower like the `#foreign` global
|
||||||
|
(`decl.zig:~1115-1137`, `.is_extern = vd.is_foreign`). New example mirroring
|
||||||
|
`examples/1205-ffi-foreign-global` with `extern`.
|
||||||
|
|
||||||
|
Stop at end of Phase 1 (do NOT start Phase 2 `export` or Part B migration). Then the
|
||||||
|
A→B gate: a unit test that `#foreign` and `extern` lower to identical IR.
|
||||||
|
|
||||||
## 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
|
||||||
@@ -58,6 +67,10 @@ historical carve-out — keep `issues/*.md` provenance, gate the live tree only.
|
|||||||
Suite green (443/633). `lock` commit.
|
Suite green (443/633). `lock` commit.
|
||||||
- (1.0b) Added `examples/1223-ffi-extern-fn.sx` + hand-authored success snapshots.
|
- (1.0b) Added `examples/1223-ffi-extern-fn.sx` + hand-authored success snapshots.
|
||||||
RED (634 ran, 1 failed — sema `body produces no value`). `xfail` commit; 1.1 greens it.
|
RED (634 ran, 1 failed — sema `body produces no value`). `xfail` commit; 1.1 greens it.
|
||||||
|
- (1.1) Wired extern fn lowering (6 edits in `decl.zig`, all declare-only routing
|
||||||
|
mirroring `foreign_expr`): `funcWantsImplicitCtx` + `declareFunction` cc +
|
||||||
|
`lazyLowerFunction`/`lowerFunction`/`lowerFunctionBodyInto` guards. 1223 green;
|
||||||
|
`declare i32 @abs(i32)` (C ABI, no ctx). Suite green (634/443). `green` commit.
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
None yet.
|
None yet.
|
||||||
|
|||||||
@@ -387,6 +387,9 @@ pub fn detectContextDecl(decls: []const *const Node) bool {
|
|||||||
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
|
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
|
||||||
if (!self.implicit_ctx_enabled) return false;
|
if (!self.implicit_ctx_enabled) return false;
|
||||||
if (fd.call_conv == .c) return false;
|
if (fd.call_conv == .c) return false;
|
||||||
|
// `extern` imports are external C symbols — C ABI, no sx context.
|
||||||
|
// (`export` defines get the same treatment in Phase 2, gap iv.)
|
||||||
|
if (fd.extern_export == .extern_) return false;
|
||||||
return switch (fd.body.data) {
|
return switch (fd.body.data) {
|
||||||
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
||||||
else => !isExportedEntryName(fd.name),
|
else => !isExportedEntryName(fd.name),
|
||||||
@@ -2078,6 +2081,12 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
|||||||
// calling convention's `...` tail. Drop the variadic param from the
|
// calling convention's `...` tail. Drop the variadic param from the
|
||||||
// IR signature (it has no C-level slot) and set is_variadic.
|
// IR signature (it has no C-level slot) and set is_variadic.
|
||||||
const is_foreign = fd.body.data == .foreign_expr;
|
const is_foreign = fd.body.data == .foreign_expr;
|
||||||
|
// Bare `extern` import: an external C symbol declared via the new linkage
|
||||||
|
// surface (empty-block placeholder body, no `foreign_expr`). It shares
|
||||||
|
// `#foreign`'s C-ABI promotion + declareExtern routing below; the optional
|
||||||
|
// `extern LIB "csym"` lib/rename axis (extern_lib/extern_name) is consumed
|
||||||
|
// in Phase 1.2. (`export` defines take the beginFunction path, not here.)
|
||||||
|
const is_extern_decl = fd.extern_export == .extern_;
|
||||||
var is_variadic = false;
|
var is_variadic = false;
|
||||||
var effective_params = fd.params;
|
var effective_params = fd.params;
|
||||||
if (is_foreign and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
|
if (is_foreign and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
|
||||||
@@ -2107,7 +2116,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
|||||||
// typed by name as `(args) -> ret` of a `#foreign` decl can be
|
// typed by name as `(args) -> ret` of a `#foreign` decl can be
|
||||||
// assigned to / passed as a `callconv(.c)` fn-pointer without a
|
// assigned to / passed as a `callconv(.c)` fn-pointer without a
|
||||||
// call-convention mismatch.
|
// call-convention mismatch.
|
||||||
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign) .c else .default;
|
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign or is_extern_decl) .c else .default;
|
||||||
|
|
||||||
// For #foreign with C name override, declare under C name and map sx name → C name
|
// For #foreign with C name override, declare under C name and map sx name → C name
|
||||||
if (is_foreign) {
|
if (is_foreign) {
|
||||||
@@ -2284,7 +2293,7 @@ pub fn lazyLowerFunction(self: *Lowering, name: []const u8) void {
|
|||||||
// a fresh ct_module via `evalComptimeString`) emits `.call` against a
|
// a fresh ct_module via `evalComptimeString`) emits `.call` against a
|
||||||
// FuncId that doesn't exist locally; the interp can't find the
|
// FuncId that doesn't exist locally; the interp can't find the
|
||||||
// foreign target and silently no-ops instead of dispatching to libc.
|
// foreign target and silently no-ops instead of dispatching to libc.
|
||||||
if (fd.body.data == .foreign_expr) {
|
if (fd.body.data == .foreign_expr or fd.extern_export == .extern_) {
|
||||||
if (self.resolveFuncByName(name) == null) {
|
if (self.resolveFuncByName(name) == null) {
|
||||||
self.declareFunction(fd, name);
|
self.declareFunction(fd, name);
|
||||||
self.lowered_functions.put(name, {}) catch {};
|
self.lowered_functions.put(name, {}) catch {};
|
||||||
@@ -2372,6 +2381,11 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId
|
|||||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||||
self.setCurrentSourceFile(func.source_file);
|
self.setCurrentSourceFile(func.source_file);
|
||||||
|
|
||||||
|
// `extern` imports are pure declarations — never promote the stub to a real
|
||||||
|
// function or lower the (empty placeholder) body. Mirrors the declare-only
|
||||||
|
// handling in lowerFunction / lazyLowerFunction.
|
||||||
|
if (fd.extern_export == .extern_) return;
|
||||||
|
|
||||||
const ret_ty = self.resolveReturnType(fd);
|
const ret_ty = self.resolveReturnType(fd);
|
||||||
|
|
||||||
if (!func.is_extern) {
|
if (!func.is_extern) {
|
||||||
@@ -2477,8 +2491,9 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i
|
|||||||
}) catch unreachable;
|
}) catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the function body is a builtin or foreign declaration (no body needed)
|
// Check if the function body is a builtin or foreign declaration (no body
|
||||||
if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) {
|
// needed). `extern` imports are declare-only too (empty placeholder body).
|
||||||
|
if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr or fd.extern_export == .extern_) {
|
||||||
// Already declared by scanDecls/declareFunction (which handles #foreign renames)
|
// Already declared by scanDecls/declareFunction (which handles #foreign renames)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user