feat(ffi-linkage): lower export fns (Phase 2.1) — example 1226 green

`export` (define + expose) now lowers to a defined C-ABI symbol with
external linkage and no implicit sx context — the four export-gap
conditions in src/ir/lower/decl.zig:

- (i) linkage: force `.external` for `extern_export == .export_` on both
  define paths (lowerFunctionBodyInto, lowerFunction), beside the
  OS-called entry points.
- (ii) C ABI: promote call_conv to `.c` on the define paths and in the
  declareFunction extern-stub cc.
- (iv) no ctx: funcWantsImplicitCtx returns false for any non-`.none`
  modifier (extern AND export), so no `__sx_ctx` slot is prepended.
- force-lower: an `export` fn is a lowering root (like `main`) in
  lowerMainAndComptime — its purpose is external consumption, so it must
  emit a body even when no sx code calls it; otherwise lazy lowering
  leaves it a bodiless `declare`.

example/1226 now builds + runs via the AOT corpus mode: the companion C
calls `sx_square` by name and prints 37 / 82. Suite green (637 corpus /
443 unit). The optional `export "csym"` rename (gap iii) is Phase 2.2.
This commit is contained in:
agra
2026-06-14 14:45:16 +03:00
parent 6a539ca057
commit a47ef20ad3

View File

@@ -387,9 +387,9 @@ pub fn detectContextDecl(decls: []const *const Node) bool {
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
if (!self.implicit_ctx_enabled) 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;
// `extern` imports and `export` defines are external C symbols —
// C ABI, no sx context (Phase 2, gap iv).
if (fd.extern_export != .none) return false;
return switch (fd.body.data) {
.foreign_expr, .builtin_expr, .compiler_expr => false,
else => !isExportedEntryName(fd.name),
@@ -1319,7 +1319,11 @@ pub fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void {
switch (decl.data) {
.const_decl => |cd| {
if (cd.value.data == .fn_decl) {
if (isExportedEntryName(cd.name)) {
// `export` defines are roots: their purpose is external
// consumption (often never called from sx), so force-lower
// them like OS-called entry points — else lazy lowering
// leaves them as bodiless `declare` stubs (Phase 2).
if (isExportedEntryName(cd.name) or cd.value.data.fn_decl.extern_export == .export_) {
self.lazyLowerFunction(cd.name);
}
} else if (cd.value.data == .comptime_expr) {
@@ -1327,7 +1331,7 @@ pub fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void {
}
},
.fn_decl => |fd| {
if (isExportedEntryName(fd.name)) {
if (isExportedEntryName(fd.name) or fd.extern_export == .export_) {
self.lazyLowerFunction(fd.name);
}
},
@@ -2117,7 +2121,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
// assigned to / passed as a `callconv(.c)` fn-pointer without a
// call-convention mismatch.
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign or is_extern_decl) .c else .default;
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign or is_extern_decl or fd.extern_export == .export_) .c else .default;
// Symbol-name override: `#foreign … "csym"` (foreign_expr.c_name) or the new
// `extern … "csym"` (fd.extern_name). Declare under the C name and map the sx
@@ -2399,8 +2403,9 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId
return;
}
func.is_extern = false; // promote from extern stub to real function
func.linkage = if (isExportedEntryName(name)) .external else .internal;
if (fd.call_conv == .c) func.call_conv = .c;
// `export` defines force external linkage + C ABI (Phase 2, gaps i+ii).
func.linkage = if (isExportedEntryName(name) or fd.extern_export == .export_) .external else .internal;
if (fd.call_conv == .c or fd.extern_export == .export_) func.call_conv = .c;
// Set inst_counter to param count (params occupy refs 0..N-1). IR params
// = AST params + 1 if the function carries `__sx_ctx` at slot 0.
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
@@ -2535,12 +2540,14 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i
// matches C `static`). isExportedEntryName lists the names the OS
// loader calls — `main`, Android NativeActivity hooks — which must
// stay externally visible.
if (isExportedEntryName(name)) {
// `export` defines force external linkage (Phase 2, gap i) alongside
// the OS-called entry points.
if (isExportedEntryName(name) or fd.extern_export == .export_) {
self.builder.currentFunc().linkage = .external;
}
// Set calling convention
if (fd.call_conv == .c) {
// Set calling convention. `export` defines promote to C ABI (gap ii).
if (fd.call_conv == .c or fd.extern_export == .export_) {
self.builder.currentFunc().call_conv = .c;
}