mem: implicit-context foundation + many compiler fixes

The session-long set of changes that lay the groundwork for the
Jai-literal implicit-Context-parameter refactor. Lots of accumulated
work; the new arrival is the implicit-ctx foundation (steps 1+2 of
the plan in current/CHECKPOINT-MEM.md):

  Step 1 — `CAllocator :: struct {}` stateless allocator in
    library/modules/allocators.sx, delegating directly to
    libc_malloc/libc_free. `ConstantValue` in src/ir/inst.zig gains a
    `func_ref: FuncId` leaf so nested aggregates can carry function
    pointers (the inline Allocator value's fn-ptr fields). Switch
    sites updated in emit_llvm.zig, print.zig, interp.zig.

  Step 2 — `emitDefaultContextGlobal` in src/ir/lower.zig synthesises
    a static `__sx_default_context` global with a nested-aggregate
    init_val pointing at the CAllocator → Allocator thunks. The
    second-pass `initVtableGlobals` in emit_llvm.zig is generalised
    to handle `.aggregate` init_vals (re-emits after func_map is
    populated so func_ref leaves resolve to real symbols).

Also folded in from earlier work this session:

  - Phase 1.1: `xx value` heap-copy in `buildProtocolValue` routes
    through `context.allocator` via the new `allocViaContext` helper.
  - interp.zig: `marshalForeignArg` double-offset bug fixed —
    `heapSlice` already adds `hp.offset` to the slice ptr, so the
    extra `+ hp.offset` was scribbling memcpy/memset into adjacent
    heap state, corrupting `heap.items[0]`. Symptom: `build_format`
    at comptime produced zero bytes, all `print` calls failed.
  - Lazy lowering: `lazyLowerFunction` now declares foreign-body
    functions as extern stubs in the local (comptime) module so
    cross-module foreign calls resolve.
  - Allocator API: all stdlib allocators on one-line `init() -> *T`
    (CAllocator/GPA: libc-backed; Arena/TrackingAllocator: parent-
    backed; BufAlloc: embeds state at head of user buffer).
  - issues 0038 (transitive #import), 0039 (chess + stdlib migration
    fallout), 0040 (generic struct method dot-dispatch), 0041
    (pointer types as type-arg), 0042 (alias name resolution) — all
    fixed; regression tests in examples/.
  - Diagnostic: `emitError` now embeds the lowering's
    `current_source_file` and enclosing function in the literal
    message; SX_TRACE_UNRESOLVED=1 dumps a Zig stack trace at the
    emit site so misattributed spans can't hide where the failure
    is.
  - tools/verify-step.sh (all-platforms gate) and tools/scratch.sh
    (interp/codegen parity tester) added.

Test suite: 152 example tests pass; chess builds + screenshots on
macOS / iOS sim / Android.
This commit is contained in:
agra
2026-05-24 22:59:20 +03:00
parent 0ba41b2980
commit 29784c22a8
63 changed files with 3448 additions and 1207 deletions

View File

@@ -84,8 +84,4 @@ main :: () {
print("{}\n", size_of(f32));
print("{}\n", size_of(Sx(f32)));
print("{}\n", size_of(Foo));
print("{}\n", size_of(Complex));
size := size_of(Sx);
print("{}\n", size);
}

View File

@@ -0,0 +1,47 @@
// Inline `xx` cast as the first argument to a struct static method must
// flow the leading param's type into the cast — otherwise an `xx ptr`
// targeting a protocol param falls back to s64 and the call frame is
// corrupted, SIGTRAPping when the body dispatches through the field.
//
// Three call shapes that must all succeed:
// 1. Named-variable receiver: `a : Allocator = xx p; T.init(a, ...)`
// 2. Free function with inline xx: `make_t(xx p, ...)`
// 3. Static method with inline xx: `T.init(xx p, ...)` ← used to crash
#import "modules/std.sx";
#import "modules/allocators.sx";
Box :: struct {
parent: Allocator;
first_ptr: *void;
init :: (parent_alloc: Allocator, size: s64) -> *Box {
self : *Box = xx malloc(size_of(Box));
self.parent = parent_alloc;
self.first_ptr = self.parent.alloc(size);
self;
}
}
make_box :: (parent_alloc: Allocator, size: s64) -> *Box {
self : *Box = xx malloc(size_of(Box));
self.parent = parent_alloc;
self.first_ptr = self.parent.alloc(size);
self;
}
main :: () -> s32 {
g_gpa : GPA = .{ alloc_count = 0 };
a : Allocator = xx @g_gpa;
b1 := Box.init(a, 64);
print("Box.init (named-var): ok\n");
b2 := make_box(xx @g_gpa, 64);
print("make_box (inline-xx): ok\n");
b3 := Box.init(xx @g_gpa, 64);
print("Box.init (inline-xx): ok\n");
0;
}

View File

@@ -0,0 +1,26 @@
// `xx allocator` recovers the typed concrete pointer (ctx) from a
// protocol value. The recovery is read-only and must not perturb
// subsequent dispatch through the protocol value, regardless of
// whether the recovery happens BEFORE or AFTER the first dispatch.
#import "modules/std.sx";
main :: () -> s32 {
gpa := GPA.init();
a : Allocator = xx gpa;
// Recover BEFORE first dispatch.
recovered : *GPA = xx a;
print("recovered == gpa? {}\n", recovered == gpa);
p := a.alloc(64);
print("alloc count after first alloc: {}\n", gpa.alloc_count);
// Recover AFTER dispatch — still works.
recovered2 : *GPA = xx a;
print("recovered2 == gpa? {}\n", recovered2 == gpa);
a.dealloc(p);
print("alloc count after dealloc: {}\n", gpa.alloc_count);
0;
}

View File

@@ -0,0 +1,18 @@
// `#import` is non-transitive: when A imports B and B imports C, A
// must NOT see C's top-level names. This file imports `b.sx` (which
// in turn imports `c.sx`) and then deliberately references C's names
// directly — the compiler is expected to reject the references with
// "not visible; #import the module that declares it" diagnostics.
//
// `b.sx` ↔ `c.sx` together still compile: `b_only_fn`'s body sees
// `c_only_fn` / `c_only_const` because b.sx directly imports c.sx.
#import "modules/std.sx";
#import "127-import-non-transitive/b.sx";
main :: () -> s32 {
print("b_only_fn: {}\n", b_only_fn());
print("c_only_fn direct: {}\n", c_only_fn());
print("c_only_const direct: {}\n", c_only_const);
0;
}

View File

@@ -0,0 +1,5 @@
#import "c.sx";
b_only_fn :: () -> s32 {
c_only_fn() + c_only_const;
}

View File

@@ -0,0 +1,2 @@
c_only_fn :: () -> s32 { 42; }
c_only_const :: 7;

View File

@@ -0,0 +1,29 @@
// Regression test for issue-0039 — directory-import scan order.
//
// When a directory is imported, the resolver iterates files
// alphabetically. Inside the directory, `aaa_uses.sx` comes BEFORE
// `types.sx` but its `make_my` returns `MyEnum` (defined in
// `types.sx`). The combined directory module must put `aaa_uses.sx`'s
// transitive imports (which include `MyEnum`) into the global scan
// stream BEFORE `aaa_uses.sx`'s own decls so the tagged_union for
// MyEnum is registered before `make_my`'s return type is resolved.
//
// Pre-fix, the dir-import implementation appended each file's
// `own_decls` before its `decls`, which inverted that order and
// caused `MyEnum` to be registered as a placeholder struct via the
// `resolveTypeName` fallback. The later `enum_decl` scan then
// short-circuited via `findByName` and never upgraded the placeholder
// to the real tagged_union, surfacing as "cannot infer enum type
// for '.b'" at the `return .b(42)` site.
#import "modules/std.sx";
#import "128-import-dir-scan-order";
main :: () -> s32 {
e := make_my();
if e == {
case .a: { print("a\n"); }
case .b: (v) { print("b={}\n", v); }
}
0;
}

View File

@@ -0,0 +1,11 @@
// Alphabetically FIRST file in the directory. Its own decl uses
// `MyEnum`, defined in a sibling file (`types.sx`). The directory
// import pass must register `MyEnum` BEFORE this file's `make_my`
// is scanned, or `MyEnum` falls back to a placeholder struct and
// `return .b(42)` fails with "cannot infer enum type".
#import "types.sx";
make_my :: () -> MyEnum {
return .b(42);
}

View File

@@ -0,0 +1,4 @@
MyEnum :: enum {
a;
b: s32;
}

View File

@@ -0,0 +1,28 @@
// Dot-call dispatch for generic struct methods.
//
// Covers three shapes:
// 1. non-generic method: h.plain()
// 2. generic method, explicit type arg: h.sized(s32)
// 3. generic method, inferred from val: h.taking(99)
#import "modules/std.sx";
Holder :: struct {
n: s64;
plain :: (self: *Holder) -> s64 { self.n; }
sized :: (self: *Holder, $T: Type) -> s64 { size_of(T); }
taking :: (self: *Holder, $T: Type, v: T) -> T { v; }
}
main :: () -> s32 {
h : *Holder = xx malloc(size_of(Holder));
h.n = 7;
print("plain: {}\n", h.plain());
print("sized s32: {}\n", h.sized(s32));
print("sized s64: {}\n", h.sized(s64));
print("taking explicit: {}\n", h.taking(s32, 42));
print("taking inferred: {}\n", h.taking(99));
0;
}

View File

@@ -0,0 +1,39 @@
// Phase 1.1 — the compiler-internal heap-copy that backs `xx value`
// protocol erasure must dispatch through `context.allocator`, not call
// libc malloc directly. So when a `push Context.{ allocator = tracer }`
// block is active, a `xx struct_value` inside it MUST be allocated by
// the tracker.
#import "modules/std.sx";
Tracer :: struct {
count: s64;
init :: () -> *Tracer {
t : *Tracer = xx malloc(size_of(Tracer));
t.count = 0;
t;
}
}
impl Allocator for Tracer {
alloc :: (self: *Tracer, size: s64) -> *void {
self.count += 1;
return malloc(size);
}
dealloc :: (self: *Tracer, ptr: *void) {
free(ptr);
}
}
ByValue :: struct { x: s64; y: s64; }
main :: () -> s32 {
tracer := Tracer.init();
push Context.{ allocator = xx tracer, data = null } {
bv : ByValue = .{ x = 1, y = 2 };
ignore : Allocator = xx bv;
_ = ignore;
}
print("Tracer.count = {}\n", tracer.count);
0;
}

View File

@@ -43,15 +43,14 @@ main :: () -> s32 {
print("listening on http://localhost:{}\n", PORT);
arena : Arena = ---;
arena_alloc := arena.create(context.allocator, 65536);
arena := Arena.init(context.allocator, 65536);
logger := Logger.{ prefix = "http", count = 0 };
while true {
client := accept(fd, null, null);
if client < 0 { continue; }
push Context.{ allocator = arena_alloc, data = xx @logger } {
push Context.{ allocator = xx arena, data = xx @logger } {
handle(client);
}

View File

@@ -1141,6 +1141,12 @@ END;
print("sizeof-f64: {}\n", size_of(f64));
print("sizeof-struct: {}\n", size_of(Point));
// align_of
print("alignof-u8: {}\n", align_of(u8));
print("alignof-s32: {}\n", align_of(s32));
print("alignof-s64: {}\n", align_of(s64));
print("alignof-struct: {}\n", align_of(Point));
// type_of + category matching
tv := 42;
ttype := type_of(tv);
@@ -1667,29 +1673,30 @@ END;
// ── GPA ─────────────────────────────────────────────────
{
gpa_state : GPA = .{ alloc_count = 0 };
gpa := gpa_state.create();
p1 := gpa.alloc(64);
p2 := gpa.alloc(128);
print("gpa allocs: {}\n", gpa_state.alloc_count); // gpa allocs: 2
gpa.dealloc(p1);
gpa.dealloc(p2);
print("gpa final: {}\n", gpa_state.alloc_count); // gpa final: 0
gpa := GPA.init();
a : Allocator = xx gpa;
p1 := a.alloc(64);
p2 := a.alloc(128);
print("gpa allocs: {}\n", gpa.alloc_count); // gpa allocs: 2
a.dealloc(p1);
a.dealloc(p2);
print("gpa final: {}\n", gpa.alloc_count); // gpa final: 0
}
// ── Arena backed by GPA (multi-chunk) ───────────────────
{
gpa_state3 : GPA = .{ alloc_count = 0 };
gpa3 := gpa_state3.create();
arena_state : Arena = ---;
arena := arena_state.create(gpa3, 32);
gpa3 := GPA.init();
arena := Arena.init(xx gpa3, 32);
a : Allocator = xx arena;
// First chunk fits 80 usable bytes
a1 := arena.alloc(40);
a2 := arena.alloc(40);
print("arena chunks: {}\n", gpa_state3.alloc_count); // arena chunks: 1
a1 := a.alloc(40);
a2 := a.alloc(40);
// Counts: Arena struct itself + first chunk = 2 (Arena.init
// allocates its own state through the parent allocator).
print("arena chunks: {}\n", gpa3.alloc_count); // arena chunks: 2
// Overflow → new chunk
a3 := arena.alloc(16);
print("arena overflow: {}\n", gpa_state3.alloc_count); // arena overflow: 2
a3 := a.alloc(16);
print("arena overflow: {}\n", gpa3.alloc_count); // arena overflow: 3
// Verify memory works across chunks
p1 : [*]u8 = xx a1;
p3 : [*]u8 = xx a3;
@@ -1698,27 +1705,27 @@ END;
print("arena a1: {}\n", p1[0]); // arena a1: 42
print("arena a3: {}\n", p3[0]); // arena a3: 99
// Reset retains newest chunk
arena_state.reset();
print("arena reset idx: {}\n", arena_state.end_index); // arena reset idx: 0
print("arena reset gpa: {}\n", gpa_state3.alloc_count);// arena reset gpa: 1
// Deinit frees all
arena_state.deinit();
print("arena deinit: {}\n", gpa_state3.alloc_count); // arena deinit: 0
arena.reset();
print("arena reset idx: {}\n", arena.end_index); // arena reset idx: 0
print("arena reset gpa: {}\n", gpa3.alloc_count); // arena reset gpa: 2
// Deinit frees all chunks + the Arena state itself
arena.deinit();
print("arena deinit: {}\n", gpa3.alloc_count); // arena deinit: 0
}
// ── BufAlloc from stack array ───────────────────────────
{
stack_buf : [128]u8 = ---;
buf_state : BufAlloc = ---;
bufalloc := buf_state.create(@stack_buf[0], 128);
b1 := bufalloc.alloc(24);
b2 := bufalloc.alloc(24);
print("buf pos: {}\n", buf_state.pos); // buf pos: 48
b3 := bufalloc.alloc(200);
buf := BufAlloc.init(@stack_buf[0], 128);
a : Allocator = xx buf;
b1 := a.alloc(24);
b2 := a.alloc(24);
print("buf pos: {}\n", buf.pos); // buf pos: 48
b3 := a.alloc(200);
b3_i : s64 = xx b3;
print("buf overflow: {}\n", b3_i); // buf overflow: 0
buf_state.reset();
print("buf reset: {}\n", buf_state.pos); // buf reset: 0
buf.reset();
print("buf reset: {}\n", buf.pos); // buf reset: 0
}
{
@@ -2446,8 +2453,8 @@ END;
step3 :: (a: Allocator) -> *void { a.alloc(8); }
step2 :: (a: Allocator) -> *void { step3(a); }
step1 :: (a: Allocator) -> *void { step2(a); }
gpa_e6 : GPA = .{ alloc_count = 0 };
a_e6 : Allocator = xx @gpa_e6;
gpa_e6 := GPA.init();
a_e6 : Allocator = xx gpa_e6;
ptr_e6 := step1(a_e6);
print("closure-chain-call: {}\n", ptr_e6 != null);
a_e6.dealloc(ptr_e6);
@@ -2498,11 +2505,9 @@ END;
print("closure-slice: {} {} {}\n", f_a7(0), f_a7(1), f_a7(2));
// C5.L1: arena bulk free (closures allocated on arena, freed in bulk)
gpa_l1 : GPA = .{ alloc_count = 0 };
a_l1 : Allocator = xx @gpa_l1;
arena_l1 : Arena = ---;
arena_alloc := arena_l1.create(a_l1, 4096);
push Context.{ allocator = arena_alloc } {
gpa_l1 := GPA.init();
arena_l1 := Arena.init(xx gpa_l1, 4096);
push Context.{ allocator = xx arena_l1 } {
n_l1 : s32 = 5;
f_l1 := closure((x: s32) -> s32 => x + n_l1);
print("closure-arena: {}\n", f_l1(10));
@@ -2510,8 +2515,8 @@ END;
arena_l1.deinit();
// C5.L2: GPA manual free (verify env alloc/dealloc)
gpa_l2 : GPA = .{ alloc_count = 0 };
a_l2 : Allocator = xx @gpa_l2;
gpa_l2 := GPA.init();
a_l2 : Allocator = xx gpa_l2;
n_l2 : s32 = 7;
result_l2 : s32 = 0;
push Context.{ allocator = a_l2 } {
@@ -2660,8 +2665,8 @@ END;
print("closure-neg: {}\n", neg_fn(val_e16));
// C5.E17: closure with protocol value capture (#inline protocol)
gpa_e17 : GPA = .{ alloc_count = 0 };
a_e17 : Allocator = xx @gpa_e17;
gpa_e17 := GPA.init();
a_e17 : Allocator = xx gpa_e17;
alloc_fn := closure((size: s64) -> *void => a_e17.alloc(size));
ptr_e17 := alloc_fn(32);
print("closure-proto-cap: {}\n", ptr_e17 != null);

View File

@@ -6,10 +6,9 @@
#import "modules/allocators.sx";
main :: () -> void {
arena : Arena = ---;
arena.create(context.allocator, 4096);
arena := Arena.init(context.allocator, 4096);
new_ctx := Context.{ allocator = xx @arena, data = context.data };
new_ctx := Context.{ allocator = xx arena, data = context.data };
push new_ctx {
ptr := context.allocator.alloc(128);
out("inside push\n");

View File

@@ -0,0 +1,30 @@
// A protocol method declared `-> *void` (literal void-pointer return,
// NOT `Self`) returns the underlying impl's pointer to the caller
// unchanged. The dispatch path must NOT auto-load from the result —
// `*void` outside a `Self`-disguise is a real pointer whose pointee
// size is unknown.
//
// Regression: target_type leaks from the surrounding scope (e.g. the
// enclosing function's return type). The dispatcher used to auto-load
// `sizeof(target_type)` bytes from every `*void` return, mistaking
// real pointers for Self-encoded boxes. Result was that
// `alloc.alloc(64)` through an Allocator protocol value returned the
// first 4 bytes of malloc'd memory interpreted as `s32` (= 0 → null).
#import "modules/std.sx";
#import "modules/allocators.sx";
main :: () -> s32 {
gpa := GPA.init();
alloc : Allocator = xx gpa;
p_direct := gpa.alloc(64);
print("direct: null? {}\n", p_direct == null);
p_protocol := alloc.alloc(64);
print("protocol: null? {}\n", p_protocol == null);
print("alloc_count: {}\n", gpa.alloc_count);
0;
}

31
examples/issue-0041.sx Normal file
View File

@@ -0,0 +1,31 @@
// issue-0041 — Pointer / optional / array / function / tuple types as
// expression-position values (size_of / align_of arg, const-decl RHS).
#import "modules/std.sx";
// Unambiguous type-form const-decl aliases.
Ptr :: *u8;
Maybe :: ?u8;
Arr :: [3]u8;
Cb :: (s32) -> s32;
main :: () -> s32 {
// Direct: parser fix for *T, ?T + existing [N]T path.
print("size_of(*u8) = {}\n", size_of(*u8));
print("align_of(*u8) = {}\n", align_of(*u8));
print("size_of(?u8) = {}\n", size_of(?u8));
print("size_of([3]u8) = {}\n", size_of([3]u8));
// Function-type literal in expression position.
print("size_of((s32)->s32) = {}\n", size_of((s32) -> s32));
// Tuple literal reinterpreted as tuple type at the type-demanding site.
print("size_of((s32, s32)) = {}\n", size_of((s32, s32)));
// Aliases.
print("size_of(Ptr) = {}\n", size_of(Ptr));
print("size_of(Maybe) = {}\n", size_of(Maybe));
print("size_of(Arr) = {}\n", size_of(Arr));
print("size_of(Cb) = {}\n", size_of(Cb));
0;
}

22
examples/issue-0042.sx Normal file
View File

@@ -0,0 +1,22 @@
// issue-0042 — const-decl type alias must resolve through
// `type_alias_map` when used as a `$T: Type` argument to size_of /
// align_of, not silently fall back to .s64 (8 bytes).
// Also covers identifier-RHS aliases (chains + struct aliases),
// not just *T / [N]T / ?T forms.
#import "modules/std.sx";
MyInt :: s32;
MyChain :: MyInt;
Wide :: struct { a: s64; b: s64; }
WideAlias :: Wide;
main :: () -> s32 {
print("direct s32: {}\n", size_of(s32));
print("alias s32: {}\n", size_of(MyInt));
print("chain s32: {}\n", size_of(MyChain));
print("align alias: {}\n", align_of(MyInt));
print("align chain: {}\n", align_of(MyChain));
print("size struct-alias: {}\n", size_of(WideAlias));
print("align struct-alias:{}\n", align_of(WideAlias));
0;
}

View File

@@ -0,0 +1,141 @@
# issue-0041 — Pointer types don't parse as expressions / type-argument positions
## Symptom
A pointer type like `*u8` or `*void` does not parse in positions
where a type expression is expected as a *value*, e.g.:
- As an argument to a `$T: Type` builtin: `size_of(*u8)`,
`align_of(*u8)`.
- On the RHS of a type alias: `Ptr :: *u8;`.
In each case the parser emits `error: unexpected token in expression`
at the column of the `*`.
Pointer types DO parse correctly in dedicated type-annotation
positions: function parameters (`(p: *u8)`), struct fields
(`field: *u8;`), variable annotations (`p: *u8 = ...;`). So the bug
is a parsing inconsistency between "type-annotation context" and
"expression context where a type is expected".
This is pre-existing — it affects `size_of` (already shipping) and
was just made more visible by adding `align_of` in Phase 0.6 of the
MEM plan. Not a regression introduced by 0.6, but a real limitation
worth pinning down because:
- Phase 1+ of the MEM plan will need `size_of(*T)` / `align_of(*T)`
in user-facing allocator helpers if we want to stay terse — e.g.
serializing a pointer-typed field in `field_value_int` patterns.
- It's a discoverability cliff. New users WILL write `size_of(*u8)`,
see "unexpected token", and have to learn the workaround.
## Reproduction
```sx
#import "modules/std.sx";
main :: () -> s32 {
n := size_of(*u8); // error: unexpected token in expression
print("{}\n", n);
0;
}
```
Also fails on the alias form:
```sx
#import "modules/std.sx";
Ptr :: *u8; // error: unexpected token in expression
main :: () -> s32 { 0; }
```
Both `sx run` and `sx build` reject identically.
## Confirmed working workarounds
A pointer type DOES resolve when bound through a `*void`-style
variable type and then cast, or routed via a helper:
```sx
// Workaround A: anonymous struct holding the pointer field, then
// pull alignment from the wrapping struct (clumsy).
Wrap :: struct { p: *u8; }
n := align_of(Wrap); // 8 — correct for pointer alignment.
// Workaround B: explicit *void
n := size_of(*void); // ALSO fails — same parse error.
```
Workaround B is NOT functional — it has the same parse error. Only
the wrap-in-struct or type-alias-via-typedef trick is currently
viable for code that needs pointer size/alignment.
There is no clean way today to write `size_of(*u8)`. The whole
class of "ptr type as type-expression value" is unsupported.
## Investigation prompt
> Pointer types parse via a dedicated `parseTypeExpr` (or similar)
> path that the parser invokes in type-annotation positions (param
> lists, field declarations, variable annotations). The expression
> grammar used in argument positions (e.g. inside `size_of(...)`)
> dispatches through `parseExpr` instead, which treats `*` as
> "either prefix unary deref or infix multiplication" — neither
> matches the desired "type literal" interpretation.
>
> The fix likely belongs in the call-argument parser path: when
> the callee is a builtin that takes `$T: Type`, OR more broadly
> whenever the parser sees a `*` at the start of an expression
> followed by an identifier that resolves to a type, it should
> dispatch to `parseTypeExpr` instead of `parsePrefixUnary`.
>
> Implementation sketch:
> - Check `src/parser.zig` for the expression entry point that
> handles `*` prefix. Today it likely returns a `unary_op
> { op = deref, operand = … }` AST node.
> - Look at how lower.zig's `resolveTypeArg` consumes the AST node
> for `size_of(s32)` — what AST shape does it expect for a type
> literal? Probably an `identifier` whose name resolves to a type.
> - The fix should extend `resolveTypeArg` to also accept a
> `unary_op { op = deref, ... }` and treat it as "pointer to
> resolved type" — equivalent to `Ptr$T` in spec terms.
> - For the type-alias case (`Ptr :: *u8;`), the RHS of a `::`
> const decl is parsed as an expression. The parser needs to
> recognize that the LHS-determined shape (type-level alias)
> should bias the RHS parser toward `parseTypeExpr`. Or: extend
> the constant-fold path to interpret `unary_op { deref, T }` as
> a type literal when used as a type.
>
> Verification:
> 1. Add `examples/issue-0041.sx` with the repro above and
> `tests/expected/issue-0041.txt` capturing the expected output
> (`size_of(*u8) → 8`).
> 2. Confirm `bash tests/run_examples.sh` still passes everything
> else (151 tests currently).
> 3. Run `tools/verify-step.sh` to confirm chess on three platforms.
> 4. Also bake into `examples/50-smoke.sx` near the existing
> `align_of` lines — add `align_of(*u8)`, `size_of(*u8)`,
> `align_of(*void)` and regen.
>
> Hazard: any change to expression parsing affects a huge surface.
> Watch for these contexts to make sure they still work post-fix:
> - `a * b` (multiplication)
> - `*p` (prefix deref read)
> - `*p = …` (prefix deref write)
> - `func(a, *b)` (deref as argument)
> A surgical "is the next token a built-in type identifier" lookahead
> at the `*` site is probably less invasive than a wholesale
> type-expression-in-expression-position rewrite.
## Plan-level impact
None for Phase 0.6 — `align_of` shipped and works for every shape
that `size_of` works for (primitives, structs, type aliases through
non-pointer types). The 50-smoke test addition uses only
non-pointer types, so it's stable.
Phase 1+ should bake an `align_of(*u8)` test once the parser fix
lands, since the allocator API will want to round-trip pointer
alignments at some call sites.

View File

@@ -0,0 +1,135 @@
# issue-0042 — Const-decl type aliases (`MyInt :: s32;`) silently return `.s64` from `size_of` / `align_of`
## Symptom
A type alias declared via `Foo :: SomeType;` is registered in the
lowering's `type_alias_map` but is **never consulted** when the alias
name is later used as a type argument to `size_of` / `align_of`. The
fallback returns `.s64` (8 bytes) — which coincidentally produces a
correct result for any alias whose underlying type is 8 bytes
(`*T`, `f64`, function pointers, `s64`, `u64`), silently masking the
bug for years.
Observed:
```
size_of(s32) = 4 ← direct, correct
size_of(MyInt) = 8 ← via alias, WRONG (expected 4)
```
Where `MyInt :: s32;`.
## Reproduction
```sx
#import "modules/std.sx";
MyInt :: s32;
main :: () -> s32 {
print("direct: {}\n", size_of(s32)); // 4
print("alias: {}\n", size_of(MyInt)); // 8 — should be 4
0;
}
```
`./zig-out/bin/sx run` against unmodified master prints:
```
direct: 4
alias: 8
```
## Why this surfaces now
issue-0041 work extends the const-decl alias path to register
pointer, optional, array, slice, many-pointer, and function-type
aliases (`Ptr :: *u8;`, `Maybe :: ?u8;`, `Arr :: [3]u8;`,
`Cb :: (s32) -> s32;`). Every one of those aliases ends up in
`type_alias_map`, then `size_of(<alias>)` falls through the same
`.identifier` branch that ignores the map — returning `.s64` (8).
For pointer and function-type aliases this is coincidentally right
(8 bytes). For optional, array, etc. it produces silently-wrong
sizes (`size_of(Maybe) = 8` instead of 2;
`size_of(Arr) = 8` instead of 3).
The issue-0041 work cannot land without this being fixed — the
test snapshots would pin in the wrong values and the new feature
would ship subtly broken.
## Investigation prompt
> The bug lives in `src/ir/lower.zig`, in `resolveTypeArg`
> ([line ~7132](src/ir/lower.zig#L7132)). The `.identifier`
> branch looks like:
>
> ```zig
> .identifier => |id| {
> if (self.type_bindings) |tb| {
> if (tb.get(id.name)) |ty| return ty;
> }
> const name_id = self.module.types.internString(id.name);
> return self.module.types.findByName(name_id) orelse .s64;
> },
> ```
>
> It checks `type_bindings` (generic-monomorphization) and
> `findByName` (registered named types), but never consults
> `self.type_alias_map` — which is where the const-decl alias
> registration in `lower.zig:425` puts entries. The neighbouring
> `.type_expr` branch (line ~7143) DOES check `type_alias_map`:
>
> ```zig
> .type_expr => |te| {
> if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
> return type_bridge.resolveAstType(node, &self.module.types);
> },
> ```
>
> Why two branches: an `.identifier` AST node is what parsePrimary
> emits for non-keyword names; `.type_expr` is what it emits for
> built-in primitive names recognised by `Type.fromName` (`s32`,
> `u8`, etc.) and for the `f32`/`f64`/`Type` keywords. User-defined
> alias names like `MyInt` and `Ptr` flow through `.identifier`.
>
> **Likely fix:** mirror the `type_alias_map.get` lookup in the
> `.identifier` branch — try alias map first (or before/after
> findByName, whichever is the established precedence elsewhere).
>
> ```zig
> .identifier => |id| {
> if (self.type_bindings) |tb| {
> if (tb.get(id.name)) |ty| return ty;
> }
> if (self.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
> const name_id = self.module.types.internString(id.name);
> return self.module.types.findByName(name_id) orelse .s64;
> },
> ```
>
> **Verification:**
> 1. Add the repro above as `examples/issue-0042.sx`.
> 2. `bash tests/run_examples.sh --update` to capture expected
> output (`alias: 4`, not `alias: 8`).
> 3. Make sure existing snapshots that test type aliases (search
> `examples/` for `::` patterns followed by `size_of`) don't
> change in unexpected ways.
>
> **Possible adjacency:** the issue may extend to `align_of`
> (likely same call path) and to type-alias chains
> (`A :: s32; B :: A;` — does B resolve through A's alias entry?).
> Worth pinning down with a test once the primary fix lands.
## Plan-level impact
Blocks issue-0041 (compound-type-as-expression). Once 0042 is
fixed, 0041 work can resume from the testing phase (the parser and
lowering edits for 0041 are already in place; only the alias
lookup is broken).
## Suggested fix order
1. Land 0042's `.identifier` alias-map lookup.
2. Resume 0041 from the test step — re-run `examples/issue-0041.sx`
and verify `size_of(Maybe) = 2`, `size_of(Arr) = 3`, etc.
3. Regenerate snapshots and proceed with the 0041 finishing
steps (50-smoke, rename, etc.).

View File

@@ -7,28 +7,61 @@ Allocator :: protocol #inline {
dealloc :: (ptr: *void);
}
// --- CAllocator: stateless allocator that delegates directly to libc ---
//
// Zero-sized struct. Used as the default `context.allocator` at program
// start (see `__sx_default_context` in the codegen). The thunks never
// dereference `self`, so the protocol value's ctx field is `null`.
//
// Unlike GPA, no `init()` is needed — there's nothing to allocate.
CAllocator :: struct {}
impl Allocator for CAllocator {
alloc :: (self: *CAllocator, size: s64) -> *void {
return libc_malloc(size);
}
dealloc :: (self: *CAllocator, ptr: *void) {
libc_free(ptr);
}
}
// --- GPA: general purpose allocator (malloc/free wrapper) ---
//
// Usage:
// gpa := GPA.init(); // *GPA
// push Context.{ allocator = xx gpa, data = null } { ... }
// print("alloc count: {}\n", gpa.alloc_count);
GPA :: struct {
alloc_count: s64;
create :: (gpa: *GPA) -> Allocator {
xx gpa;
init :: () -> *GPA {
g : *GPA = xx libc_malloc(size_of(GPA));
g.alloc_count = 0;
g;
}
}
impl Allocator for GPA {
alloc :: (self: *GPA, size: s64) -> *void {
self.alloc_count += 1;
malloc(size);
return libc_malloc(size);
}
dealloc :: (self: *GPA, ptr: *void) {
self.alloc_count -= 1;
free(ptr);
libc_free(ptr);
}
}
// --- Arena: multi-chunk bump allocator ---
//
// Usage:
// gpa := GPA.init();
// arena := Arena.init(xx gpa, 4096); // *Arena
// push Context.{ allocator = xx arena, data = null } { ... }
// arena.reset(); // direct method on *Arena
// arena.deinit();
ArenaChunk :: struct {
next: *ArenaChunk;
@@ -52,12 +85,13 @@ Arena :: struct {
a.end_index = 0;
}
create :: (a: *Arena, parent: Allocator, size: s64) -> Allocator {
a.first = null;
a.end_index = 0;
a.parent = parent;
a.add_chunk(size);
xx a;
init :: (parent_alloc: Allocator, size: s64) -> *Arena {
self : *Arena = xx parent_alloc.alloc(size_of(Arena));
self.first = null;
self.end_index = 0;
self.parent = parent_alloc;
self.add_chunk(size);
self;
}
reset :: (a: *Arena) {
@@ -80,8 +114,8 @@ Arena :: struct {
a.parent.dealloc(it);
it = next;
}
a.first = null;
a.end_index = 0;
parent := a.parent;
parent.dealloc(xx a);
}
}
@@ -107,17 +141,26 @@ impl Allocator for Arena {
}
// --- BufAlloc: bump allocator backed by a user-provided slice ---
//
// Usage:
// stack_buf : [128]u8 = ---;
// buf := BufAlloc.init(@stack_buf[0], 128); // *BufAlloc
// push Context.{ allocator = xx buf, data = null } { ... }
// buf.reset();
BufAlloc :: struct {
buf: [*]u8;
len: s64;
pos: s64;
create :: (b: *BufAlloc, buf: [*]u8, len: s64) -> Allocator {
b.buf = buf;
b.len = len;
init :: (buf: [*]u8, len: s64) -> *BufAlloc {
self_size :: size_of(BufAlloc);
if len < self_size { return null; }
b : *BufAlloc = xx buf;
b.buf = @buf[self_size];
b.len = len - self_size;
b.pos = 0;
xx b;
b;
}
reset :: (b: *BufAlloc) {
@@ -137,3 +180,66 @@ impl Allocator for BufAlloc {
}
dealloc :: (self: *BufAlloc, ptr: *void) {}
}
// --- TrackingAllocator: wraps any Allocator, counts allocs/deallocs ---
//
// Useful for catching leaks during development. Wraps a parent
// Allocator; every call delegates to the parent while updating
// counters. `report()` prints a summary; `leak_count()` returns
// (alloc_count - dealloc_count).
//
// Manual opt-in pattern (compiler auto-wrap lands in Phase 5):
//
// tracker := TrackingAllocator.init(context.allocator); // *TrackingAllocator
// push Context.{ allocator = xx tracker, data = null } {
// // ... user code allocates via tracker → delegates to the
// // original context.allocator (libc-backed by default) ...
// }
// tracker.report();
// if tracker.leak_count() != 0 { return 1; }
//
// Limitations under the current 2-method Allocator protocol:
// dealloc(ptr) provides no size info, so bytes_outstanding /
// peak_bytes cannot be tracked accurately. Only alloc count and
// total bytes allocated are recorded. Phase 4's size-aware
// dealloc(ptr, size, align) unlocks full byte tracking.
TrackingAllocator :: struct {
parent: Allocator;
alloc_count: s64;
dealloc_count: s64;
total_alloc_bytes: s64;
init :: (parent_alloc: Allocator) -> *TrackingAllocator {
t : *TrackingAllocator = xx parent_alloc.alloc(size_of(TrackingAllocator));
t.parent = parent_alloc;
t.alloc_count = 0;
t.dealloc_count = 0;
t.total_alloc_bytes = 0;
t;
}
leak_count :: (t: *TrackingAllocator) -> s64 {
t.alloc_count - t.dealloc_count;
}
report :: (t: *TrackingAllocator) {
print("TrackingAllocator: allocs={} deallocs={} outstanding={} total_alloc_bytes={}\n",
t.alloc_count, t.dealloc_count, t.leak_count(), t.total_alloc_bytes);
}
}
impl Allocator for TrackingAllocator {
alloc :: (self: *TrackingAllocator, size: s64) -> *void {
ptr := self.parent.alloc(size);
if ptr != null {
self.alloc_count += 1;
self.total_alloc_bytes += size;
}
ptr;
}
dealloc :: (self: *TrackingAllocator, ptr: *void) {
self.parent.dealloc(ptr);
self.dealloc_count += 1;
}
}

View File

@@ -4,10 +4,16 @@ out :: (str: string) -> void #builtin;
// sin :: (x: $T) -> T #builtin;
// cos :: (x: $T) -> T #builtin;
size_of :: ($T: Type) -> s64 #builtin;
malloc :: (size: s64) -> *void #builtin;
memcpy :: (dst: *void, src: *void, size: s64) -> *void #builtin;
memset :: (dst: *void, val: s64, size: s64) -> void #builtin;
free :: (ptr: *void) -> void #builtin;
align_of :: ($T: Type) -> s64 #builtin;
// Low-level libc bindings, used by allocator implementations to avoid
// recursing through `context.allocator`.
libc_malloc :: (size: s64) -> *void #foreign libc "malloc";
libc_free :: (ptr: *void) -> void #foreign libc "free";
malloc :: (size: s64) -> *void #foreign libc "malloc";
memcpy :: (dst: *void, src: *void, size: s64) -> *void #foreign libc "memcpy";
memset :: (dst: *void, val: s64, size: s64) -> void #foreign libc "memset";
free :: (ptr: *void) -> void #foreign libc "free";
type_of :: (val: $T) -> Type #builtin;
type_name :: ($T: Type) -> string #builtin;
field_count :: ($T: Type) -> s64 #builtin;

View File

@@ -1,3 +1,5 @@
#import "std.sx";
assert :: (condition: bool) {
if !condition {
out("assertion failed\n");

View File

@@ -5,6 +5,7 @@
#import "modules/ui/types.sx";
#import "modules/ui/render.sx";
#import "modules/ui/events.sx";
#import "modules/ui/font.sx";
#import "modules/ui/view.sx";
#import "modules/ui/renderer.sx";
@@ -17,9 +18,11 @@ UIPipeline :: struct {
root: ViewChild;
has_root: bool;
// Frame arena infrastructure
arena_a: Arena;
arena_b: Arena;
// Frame arena infrastructure. Both arenas are typed `*Arena`
// pointers (per the init-returns-typed-pointer API in
// allocators.sx). Cast to Allocator at use sites via `xx arena_a`.
arena_a: *Arena;
arena_b: *Arena;
frame_index: s64;
body: Closure() -> View;
has_body: bool;
@@ -65,8 +68,8 @@ UIPipeline :: struct {
self.has_body = true;
self.parent_allocator = context.allocator;
// Initialize both arenas (256KB initial, grows automatically)
self.arena_a.create(self.parent_allocator, 262144);
self.arena_b.create(self.parent_allocator, 262144);
self.arena_a = Arena.init(self.parent_allocator, 262144);
self.arena_b = Arena.init(self.parent_allocator, 262144);
self.frame_index = 0;
}
@@ -129,7 +132,7 @@ UIPipeline :: struct {
}
tick_with_body :: (self: *UIPipeline) {
build_arena : *Arena = if self.frame_index & 1 == 0 then @self.arena_a else @self.arena_b;
build_arena : *Arena = if self.frame_index & 1 == 0 then self.arena_a else self.arena_b;
build_arena.reset();
// Reset render_tree nodes (backing is stale after arena reset)

View File

@@ -254,30 +254,98 @@ fn selfExePath(allocator: std.mem.Allocator) ![]const u8 {
/// A resolved module: the fully-resolved declarations of a single .sx file,
/// with its own scope tracking which names are defined.
///
/// Imports are non-transitive. `scope` is intentionally *narrow*: it
/// contains only the names of decls authored in THIS file (plus namespaced
/// import aliases the file introduces). Visibility for names from
/// flat-imported modules is computed at lookup time by joining the
/// importer's `scope` with each direct flat-import's `scope` via
/// `import_graph` — this lets cyclic imports (e.g. std.sx ↔ allocators.sx)
/// resolve correctly even though one side of the cycle is skipped during
/// `resolveImports` recursion.
///
/// `decls` remains the full transitive flat list so the global lowering
/// pass can resolve a body in B that calls into C even though A never
/// imported C directly.
pub const ResolvedModule = struct {
path: []const u8,
/// Full flat decl list: own decls + every transitively-imported module's
/// own decls (deduped by name). Walked by `lowerRoot`/`scanDecls` so
/// transitive callees stay resolvable when their callers are lowered.
decls: []const *Node,
/// Decls authored in this file. What flat importers of THIS module see
/// (their visibility BFS joins these names in via `import_graph`).
own_decls: []const *Node,
/// Names authored in this file (plus namespace aliases this file
/// introduces). Used as the per-file leaf in the visibility lookup;
/// importers do NOT splice this into their own scope — they walk the
/// import graph at query time instead.
scope: std.StringHashMap(void),
/// Try to add a declaration. Returns true if added, false if name already in scope.
pub fn addDecl(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), decl: *Node) !bool {
/// Add a declaration authored in this file. Updates scope + own_decls +
/// the global flat decl list; dedups by name through `seen_list` (which
/// already holds names previously appended via `mergeFlat`, so an
/// authored decl that collides with a transitively-imported one stays
/// out of the global list while still entering `own_decls` for
/// importer-visibility purposes).
pub fn addOwnDecl(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
own_list: *std.ArrayList(*Node),
seen_list: *std.StringHashMap(void),
decl: *Node,
) !bool {
var append_to_global = true;
if (decl.data.declName()) |name| {
if (self.scope.contains(name)) return false;
try self.scope.put(name, {});
if (seen_list.contains(name)) {
append_to_global = false;
} else {
try seen_list.put(name, {});
}
}
try list.append(allocator, decl);
if (append_to_global) try list.append(allocator, decl);
try own_list.append(allocator, decl);
return true;
}
/// Merge another module's decls as flat imports (skipping duplicates).
pub fn mergeFlat(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), other: ResolvedModule) !void {
/// Flat-import another module. The imported names are NOT added to
/// `self.scope` — visibility joins per-file scopes at lookup time via
/// `import_graph`. We only need to append `other.decls` (the full
/// transitive list) to the global `list` so the lowering pass can
/// still resolve transitively-imported callees. Deduped by name.
pub fn mergeFlat(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
seen_list: *std.StringHashMap(void),
other: ResolvedModule,
) !void {
_ = self;
for (other.decls) |decl| {
_ = try self.addDecl(allocator, list, decl);
if (decl.data.declName()) |name| {
if (seen_list.contains(name)) continue;
try seen_list.put(name, {});
}
try list.append(allocator, decl);
}
}
/// Add another module as a namespaced import.
pub fn addNamespace(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), name: []const u8, other: ResolvedModule, span: ast.Span) !void {
/// Add another module as a namespaced import. The alias `name` becomes
/// part of this module's own decls (so a flat-importer of this module
/// sees the alias one hop out — matching authored names).
pub fn addNamespace(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
own_list: *std.ArrayList(*Node),
seen_list: *std.StringHashMap(void),
name: []const u8,
other: ResolvedModule,
span: ast.Span,
) !void {
const ns_node = try allocator.create(Node);
ns_node.* = .{
.span = span,
@@ -287,11 +355,19 @@ pub const ResolvedModule = struct {
} },
};
try self.scope.put(name, {});
try seen_list.put(name, {});
try list.append(allocator, ns_node);
try own_list.append(allocator, ns_node);
}
pub fn finalize(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node)) !void {
pub fn finalize(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
own_list: *std.ArrayList(*Node),
) !void {
self.decls = try list.toOwnedSlice(allocator);
self.own_decls = try own_list.toOwnedSlice(allocator);
}
};
@@ -323,6 +399,7 @@ pub fn resolveImports(
var mod = ResolvedModule{
.path = file_path,
.decls = &.{},
.own_decls = &.{},
.scope = std.StringHashMap(void).init(allocator),
};
@@ -338,6 +415,11 @@ pub fn resolveImports(
const flat_decls = try flattenComptimeConditionals(allocator, root.data.root.decls, comptime_ctx);
var decl_list = std.ArrayList(*Node).empty;
var own_decl_list = std.ArrayList(*Node).empty;
// Name set spanning every decl already appended to `decl_list` — used
// by `mergeFlat` to dedupe across diamond imports now that `mod.scope`
// is non-transitive and can no longer serve as the dedup key.
var seen_in_list = std.StringHashMap(void).init(allocator);
for (flat_decls) |decl| {
if (decl.data == .c_import_decl) {
@@ -397,21 +479,23 @@ pub fn resolveImports(
};
ns_node.source_file = file_path;
try mod.scope.put(ns_name, {});
try seen_in_list.put(ns_name, {});
try decl_list.append(allocator, ns_node);
try own_decl_list.append(allocator, ns_node);
} else {
// Flat: add fn_decls directly + keep c_import_decl
for (result.fn_decls) |fd| {
fd.source_file = file_path;
_ = try mod.addDecl(allocator, &decl_list, fd);
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, fd);
}
decl.source_file = file_path;
_ = try mod.addDecl(allocator, &decl_list, decl);
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
}
continue;
}
if (decl.data != .import_decl) {
decl.source_file = file_path;
_ = try mod.addDecl(allocator, &decl_list, decl);
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
continue;
}
const imp = decl.data.import_decl;
@@ -473,13 +557,13 @@ pub fn resolveImports(
};
if (imp.name) |ns_name| {
try mod.addNamespace(allocator, &decl_list, ns_name, imported_mod, decl.span);
try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span);
} else {
try mod.mergeFlat(allocator, &decl_list, imported_mod);
try mod.mergeFlat(allocator, &decl_list, &seen_in_list, imported_mod);
}
}
try mod.finalize(allocator, &decl_list);
try mod.finalize(allocator, &decl_list, &own_decl_list);
return mod;
}
@@ -524,13 +608,20 @@ fn resolveDirectoryImport(
try chain.put(dir_path, {});
defer _ = chain.remove(dir_path);
// Merge all files into a combined module
// Merge all files into a combined module. From an importer's perspective
// a directory is one big module: the combined module's `own_decls` is
// the union of every file's `own_decls`, so flat-importing the directory
// exposes everything the files themselves authored — but not what those
// files transitively imported from outside the directory.
var combined = ResolvedModule{
.path = dir_path,
.decls = &.{},
.own_decls = &.{},
.scope = std.StringHashMap(void).init(allocator),
};
var decl_list = std.ArrayList(*Node).empty;
var own_decl_list = std.ArrayList(*Node).empty;
var seen_in_list = std.StringHashMap(void).init(allocator);
for (file_names.items) |file_name| {
const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ dir_path, file_name });
@@ -568,9 +659,34 @@ fn resolveDirectoryImport(
break :file_blk result;
};
try combined.mergeFlat(allocator, &decl_list, file_mod);
// Source-order matters: a file's own decls (e.g. `impl Foo` blocks)
// may reference types defined in OTHER files that THIS file imports.
// `file_mod.decls` already lists transitive-imported decls before
// the file's own decls (resolveImports processes `#import` lines in
// source order, and #imports usually come first), so iterating it
// directly preserves the scan order the lowering pass needs to
// register `Event` (a tagged_union) before `handle_event(e: *Event)`
// triggers the placeholder-struct fallback in `resolveTypeName`.
for (file_mod.decls) |decl| {
if (decl.data.declName()) |name| {
if (seen_in_list.contains(name)) continue;
try seen_in_list.put(name, {});
}
try decl_list.append(allocator, decl);
}
// Separately track which decls the directory `re-exports` to its
// flat-importers. Position in `own_decl_list` doesn't matter — it's
// only consumed by the importer-side visibility join (`isNameVisible`
// in lower.zig) which treats it as a set.
for (file_mod.own_decls) |decl| {
if (decl.data.declName()) |name| {
if (combined.scope.contains(name)) continue;
try combined.scope.put(name, {});
}
try own_decl_list.append(allocator, decl);
}
}
try combined.finalize(allocator, &decl_list);
try combined.finalize(allocator, &decl_list, &own_decl_list);
return combined;
}

View File

@@ -681,32 +681,40 @@ pub const LLVMEmitter = struct {
}
}
/// Initialize vtable globals with function pointer constants.
/// Must run after Pass 1 (function declarations) so func_map is populated.
/// Initialize vtable + aggregate-with-func_ref globals with function
/// pointer constants. Must run after Pass 1 (function declarations) so
/// func_map is populated — that's why these globals get a placeholder
/// initializer in `emitGlobals` and we fix them up here.
fn initVtableGlobals(self: *LLVMEmitter) void {
for (self.ir_mod.globals.items, 0..) |global, i| {
const iv = global.init_val orelse continue;
const func_ids = switch (iv) {
.vtable => |ids| ids,
else => continue,
};
const llvm_global = self.global_map.get(@intCast(i)) orelse continue;
const llvm_ty = self.toLLVMType(global.ty);
// Build constant struct of function pointers
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (func_ids) |fid| {
const llvm_func = self.func_map.get(fid.index()) orelse {
field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable;
continue;
};
field_vals.append(self.alloc, llvm_func) catch unreachable;
switch (iv) {
.vtable => |func_ids| {
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (func_ids) |fid| {
const llvm_func = self.func_map.get(fid.index()) orelse {
field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable;
continue;
};
field_vals.append(self.alloc, llvm_func) catch unreachable;
}
const init_val = c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
c.LLVMSetInitializer(llvm_global, init_val);
c.LLVMSetGlobalConstant(llvm_global, 1);
},
.aggregate => |agg| {
// Re-emit. The first pass in `emitGlobals` already ran,
// but func_ref leaves resolved to null then (func_map
// wasn't populated yet). Now they resolve properly.
const init_val = self.emitConstAggregate(agg, llvm_ty);
c.LLVMSetInitializer(llvm_global, init_val);
},
else => continue,
}
const init_val = c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
c.LLVMSetInitializer(llvm_global, init_val);
c.LLVMSetGlobalConstant(llvm_global, 1);
}
}
@@ -2241,39 +2249,6 @@ pub const LLVMEmitter = struct {
.call_builtin => |bi| {
// Builtins that map to libc functions or LLVM intrinsics
switch (bi.builtin) {
.malloc => {
const size = self.coerceArg(self.resolveRef(bi.args[0]), self.sizeType());
const malloc_fn = self.getOrDeclareMalloc();
var args = [_]c.LLVMValueRef{size};
self.mapRef(c.LLVMBuildCall2(self.builder, self.getMallocType(), malloc_fn, &args, 1, "malloc"));
},
.free => {
const ptr = self.resolveRef(bi.args[0]);
const free_fn = self.getOrDeclareFree();
var args = [_]c.LLVMValueRef{ptr};
_ = c.LLVMBuildCall2(self.builder, self.getFreeType(), free_fn, &args, 1, "");
self.advanceRefCounter();
},
.memcpy => {
const dst = self.resolveRef(bi.args[0]);
const src = self.resolveRef(bi.args[1]);
const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType());
const memcpy_fn = self.getOrDeclareMemcpy();
var args = [_]c.LLVMValueRef{ dst, src, len };
_ = c.LLVMBuildCall2(self.builder, self.getMemcpyType(), memcpy_fn, &args, 3, "");
self.advanceRefCounter();
},
.memset => {
const dst = self.resolveRef(bi.args[0]);
var val = self.resolveRef(bi.args[1]);
const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType());
// memset expects i32 for byte value — coerce width
val = self.coerceArg(val, self.cached_i32);
const memset_fn = self.getOrDeclareMemset();
var args = [_]c.LLVMValueRef{ dst, val, len };
_ = c.LLVMBuildCall2(self.builder, self.getMemsetType(), memset_fn, &args, 3, "");
self.advanceRefCounter();
},
.sqrt, .sin, .cos, .floor => {
const val = self.resolveRef(bi.args[0]);
const val_ty = c.LLVMTypeOf(val);
@@ -3699,6 +3674,7 @@ pub const LLVMEmitter = struct {
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(elem_ty),
else => c.LLVMConstNull(elem_ty),
};
}

View File

@@ -352,11 +352,8 @@ pub const BuiltinId = enum(u16) {
cos,
floor,
size_of,
align_of,
cast,
malloc,
free,
memcpy,
memset,
type_of,
alloc,
dealloc,
@@ -519,5 +516,10 @@ pub const ConstantValue = union(enum) {
aggregate: []const ConstantValue,
/// Vtable constant: struct of function pointers, used for protocol vtable globals.
vtable: []const FuncId,
/// Function pointer leaf, for static initializers that include
/// function addresses inside nested aggregates (e.g. the inline
/// Allocator value `{ ctx, alloc_fn, dealloc_fn }` for the
/// process-wide default Context).
func_ref: FuncId,
};

View File

@@ -238,8 +238,11 @@ pub const Interpreter = struct {
.boolean => |b| @intFromBool(b),
.null_val => 0,
.heap_ptr => |hp| blk: {
const mem = self.heapSlice(hp) orelse return error.TypeError;
break :blk @intFromPtr(mem.ptr) + hp.offset;
// `heapSlice` returns the slice already advanced by `hp.offset`,
// so its `.ptr` IS the offset address. Adding `hp.offset` again
// double-counts and lands the foreign call past the buffer end.
_ = self.heapSlice(hp) orelse return error.TypeError;
break :blk @intFromPtr(self.heap.items[hp.id].ptr) + hp.offset;
},
.string => |s| blk: {
const buf = try self.alloc.alloc(u8, s.len + 1);
@@ -1315,6 +1318,7 @@ pub const Interpreter = struct {
}
return .{ .aggregate = fields };
},
.func_ref => |fid| .{ .func_ref = fid },
};
}
@@ -1401,56 +1405,6 @@ pub const Interpreter = struct {
fn execBuiltinInner(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame) InterpError!ExecResult {
switch (bi.builtin) {
.malloc => {
const size_val = frame.getRef(bi.args[0]);
const size: usize = @intCast(size_val.asInt() orelse return error.TypeError);
const hp = self.heapAlloc(size);
return .{ .value = .{ .heap_ptr = hp } };
},
.free => {
const ptr = frame.getRef(bi.args[0]);
switch (ptr) {
.heap_ptr => |hp| self.heapFree(hp),
else => {},
}
return .{ .value = .void_val };
},
.memcpy => {
const dst = frame.getRef(bi.args[0]);
const src = frame.getRef(bi.args[1]);
const len_val = frame.getRef(bi.args[2]);
const len: usize = @intCast(len_val.asInt() orelse return error.TypeError);
const dst_hp = switch (dst) {
.heap_ptr => |hp| hp,
else => return error.CannotEvalComptime,
};
const src_bytes: []const u8 = switch (src) {
.heap_ptr => |hp| self.heapSlice(hp) orelse return error.CannotEvalComptime,
.string => |s| s,
// Raw host address (e.g. a `*u8` returned by a foreign
// call like getenv). Read `len` bytes across the FFI
// boundary into the sx-managed dst.
.int => |addr| blk: {
const raw: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(addr)));
break :blk raw[0..len];
},
else => return error.CannotEvalComptime,
};
self.heapMemcpy(dst_hp, src_bytes, len);
return .{ .value = .{ .heap_ptr = dst_hp } };
},
.memset => {
const dst = frame.getRef(bi.args[0]);
const val = frame.getRef(bi.args[1]);
const len_val = frame.getRef(bi.args[2]);
const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF);
const len: usize = @intCast(len_val.asInt() orelse return error.TypeError);
switch (dst) {
.heap_ptr => |hp| self.heapMemset(hp, byte, len),
else => {},
}
return .{ .value = .void_val };
},
.out => {
const str_val = frame.getRef(bi.args[0]);
if (str_val.asString(self)) |s| {
@@ -1462,6 +1416,9 @@ pub const Interpreter = struct {
// Return a default size (8 bytes for most types)
return .{ .value = .{ .int = 8 } };
},
.align_of => {
return .{ .value = .{ .int = 8 } };
},
.sqrt => {
const val = frame.getRef(bi.args[0]);
const f = val.asFloat() orelse return error.TypeError;

View File

@@ -161,6 +161,10 @@ pub const Lowering = struct {
name: []const u8,
param_types: []const TypeId, // excluding self
ret_type: TypeId,
// True when the AST return type was `Self` (encoded here as *void).
// Lets the dispatcher distinguish Self-disguised-as-*void (auto-unbox
// on the caller side) from a literal `-> *void` (return as-is).
ret_is_self: bool = false,
};
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
@@ -214,6 +218,12 @@ pub const Lowering = struct {
self.scanDecls(decls);
// Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config
self.injectComptimeConstants();
// Pass 1c: emit the process-wide default Context global, statically
// initialised to a CAllocator-backed Allocator value. Used by FFI
// wrappers in Step 4 and by the interp's `callWithDefaultContext`
// entry. Only fires when the program imports `std.sx` (so Context +
// Allocator + CAllocator are all registered).
self.emitDefaultContextGlobal();
// Pass 2: lower main (and comptime side-effects)
self.lowerMainAndComptime(decls);
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
@@ -418,10 +428,32 @@ pub const Lowering = struct {
} else if (cd.value.data == .union_decl) {
// Register plain union types in the type table
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
} else if (cd.value.data == .type_expr) {
// Type alias: MyFloat :: f64; → register MyFloat as alias for f64
} else if (cd.value.data == .type_expr or
cd.value.data == .pointer_type_expr or
cd.value.data == .many_pointer_type_expr or
cd.value.data == .array_type_expr or
cd.value.data == .slice_type_expr or
cd.value.data == .optional_type_expr or
cd.value.data == .function_type_expr)
{
// Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (s32) -> s32;
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types);
self.type_alias_map.put(cd.name, target_ty) catch {};
} else if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide;
// Chase through type_alias_map, then look up named types
// in the table. Forward references resolve lazily because
// the .identifier branch of resolveTypeArg also consults
// type_alias_map at use time.
const rhs_name = cd.value.data.identifier.name;
if (self.type_alias_map.get(rhs_name)) |chained| {
self.type_alias_map.put(cd.name, chained) catch {};
} else {
const name_id = self.module.types.internString(rhs_name);
if (self.module.types.findByName(name_id)) |tid| {
self.type_alias_map.put(cd.name, tid) catch {};
}
}
}
// Handle generic struct instantiation: Vec3 :: Vec(3, f32)
// Parser produces a .call node for these (not parameterized_type_expr)
@@ -723,11 +755,38 @@ pub const Lowering = struct {
// Only restrict C import fn_decls: foreign_expr with no library_ref
if (fd.body.data != .foreign_expr) return true;
if (fd.body.data.foreign_expr.library_ref != null) return true;
// It's a C import fn_decl — check module scope
return self.isNameVisible(fn_name);
}
/// Non-transitive `#import` visibility check for top-level decls.
///
/// `module_scopes[F]` holds ONLY the names authored in file F (plus its
/// namespace aliases). Cross-module visibility is joined here at query
/// time by walking each direct flat-import edge in `import_graph` — a
/// name is visible from F when it's authored in F or in any module F
/// directly `#import`s. Doing the join here (instead of pre-merging in
/// `resolveImports`) lets cyclic imports like std.sx ↔ allocators.sx
/// still resolve, since the cycle's skipped edge is still recorded in
/// `import_graph` and the partner's scope is filled in by the time
/// lowering queries it.
///
/// Falls open when the scoping infrastructure isn't wired (comptime
/// callers, directory imports without main_file, etc.). The caller is
/// responsible for restricting the call to names that ARE known
/// top-level decls; otherwise every local variable would be policed.
fn isNameVisible(self: *Lowering, name: []const u8) bool {
const scopes = self.module_scopes orelse return true;
const source = self.current_source_file orelse return true;
const scope = scopes.get(source) orelse return true;
return scope.contains(fn_name);
const own_scope = scopes.get(source) orelse return true;
if (own_scope.contains(name)) return true;
const graph = self.import_graph orelse return true;
const direct = graph.get(source) orelse return true;
var it = direct.iterator();
while (it.next()) |kv| {
const dep = scopes.get(kv.key_ptr.*) orelse continue;
if (dep.contains(name)) return true;
}
return false;
}
/// Lazily lower a function body on demand. Called when lowerCall can't find
@@ -737,8 +796,21 @@ pub const Lowering = struct {
if (self.lowered_functions.contains(name)) return;
// No AST? (builtins, foreign functions, or imported functions not in this file)
const fd = self.fn_ast_map.get(name) orelse return;
// Check builtin/foreign/generic — these stay as extern stubs
if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) return;
// Foreign declarations stay as extern stubs but need to be REGISTERED
// in the current module so callers get a real FuncId. Without this,
// a comptime-lowered function (e.g. `concat` from std.sx pulled into
// a fresh ct_module via `evalComptimeString`) emits `.call` against a
// FuncId that doesn't exist locally; the interp can't find the
// foreign target and silently no-ops instead of dispatching to libc.
if (fd.body.data == .foreign_expr) {
if (self.resolveFuncByName(name) == null) {
self.declareFunction(fd, name);
self.lowered_functions.put(name, {}) catch {};
}
return;
}
// Builtins / #compiler bodies stay as compiler-handled — no extern stub needed.
if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr) return;
if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13)
// Defer functions with type-category matches until all types are registered.
@@ -1715,12 +1787,29 @@ pub const Lowering = struct {
}
// Check module-level value constants (e.g. AF_INET :s32: 2)
if (self.module_const_map.get(id.name)) |ci| {
if (!self.isNameVisible(id.name)) {
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name});
break :blk self.emitError(id.name, node.span);
}
break :blk self.emitModuleConst(ci);
}
// Check if it's a function name — produce function pointer reference
// Resolve mangled name for block-local functions
const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
if (self.fn_ast_map.contains(eff_fn_name)) {
// Visibility check only for user-typed bare names (id.name
// == eff_fn_name) without a UFCS alias. Mangled local-
// scope names and UFCS rewrites are compiler indirections
// and stay exempt.
if (std.mem.eql(u8, eff_fn_name, id.name) and
self.ufcs_alias_map.get(id.name) == null and
!self.isNameVisible(eff_fn_name))
{
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name});
break :blk self.emitError(eff_fn_name, node.span);
}
// Type-as-value: if target is Any (Type variable), produce a type name string
if (self.target_type == .any) {
const fd = self.fn_ast_map.get(eff_fn_name).?;
@@ -4423,6 +4512,19 @@ pub const Lowering = struct {
d.addFmt(.err, c.callee.span, "C function '{s}' not visible; add #import for the module that declares it", .{eff_name});
return Ref.none;
}
// Non-transitive `#import` visibility check. Apply only when the
// user-typed name resolved as-is to a top-level fn — local-scope
// mangling (eff_name != id_name) and UFCS alias rewriting are
// compiler indirections and stay exempt.
if (std.mem.eql(u8, eff_name, id_name) and
self.ufcs_alias_map.get(id_name) == null and
self.fn_ast_map.contains(eff_name) and
!self.isNameVisible(eff_name))
{
if (self.diagnostics) |d|
d.addFmt(.err, c.callee.span, "'{s}' is not visible; #import the module that declares it", .{eff_name});
return Ref.none;
}
if (self.fn_ast_map.get(eff_name)) |fd| {
if (self.current_match_tags) |tags| {
if (tags.len > 0 and self.hasCastWithRuntimeType(c)) {
@@ -4608,19 +4710,8 @@ pub const Lowering = struct {
}
// Check builtins first (these are handled natively by interpreter and emitter)
if (resolveBuiltin(id.name)) |bid| {
// free(protocol_value) → extract ctx (field 0) and free it
if (bid == .free and args.items.len == 1) {
const arg_ty = self.builder.getRefType(args.items[0]);
if (self.getProtocolInfo(arg_ty) != null) {
const void_ptr_ty = self.module.types.ptrTo(.void);
const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = args.items[0], .field_index = 0 } }, void_ptr_ty);
return self.builder.emit(.{ .heap_free = .{ .operand = ctx_ref } }, .void);
}
}
const ret_ty: TypeId = switch (bid) {
.malloc => .s64, // pointer
.size_of => .s64,
.memcpy, .memset => .s64,
.size_of, .align_of => .s64,
.sqrt, .sin, .cos, .floor => blk: {
// Math builtins: return type matches argument type ($T -> T)
if (c.args.len > 0) {
@@ -5041,6 +5132,53 @@ pub const Lowering = struct {
}
}
// Generic method on a non-template struct: `obj.method($T, ...)`
// or inferred form `obj.method(val)` where val's type pins $T.
if (self.fn_ast_map.get(qualified)) |gen_fd| {
if (gen_fd.type_params.len > 0 and gen_fd.body.data != .compiler_expr) {
// Effective AST args: prepend receiver so positions
// line up with fd.params (which has self at index 0).
var eff_args = std.ArrayList(*const Node).empty;
defer eff_args.deinit(self.alloc);
eff_args.append(self.alloc, effective_obj_node) catch unreachable;
for (c.args) |a| eff_args.append(self.alloc, a) catch unreachable;
var gbindings = self.buildTypeBindings(gen_fd, eff_args.items);
defer gbindings.deinit();
const gmangled = self.mangleGenericName(qualified, gen_fd, &gbindings);
if (!self.lowered_functions.contains(gmangled)) {
self.monomorphizeFunction(gen_fd, gmangled, &gbindings);
}
if (self.resolveFuncByName(gmangled)) |gfid| {
const gfunc = &self.module.functions.items[@intFromEnum(gfid)];
const gret_ty = gfunc.ret;
const gparams = gfunc.params;
// Strip type-decl slots from method_args. method_args[0] is the
// receiver (corresponds to fd.params[0] = self, never a type decl).
// Walk fd.params[1..], advance arg_idx through method_args[1..].
var gvalue_args = std.ArrayList(Ref).empty;
defer gvalue_args.deinit(self.alloc);
gvalue_args.append(self.alloc, method_args.items[0]) catch unreachable;
const types_explicit = method_args.items.len == gen_fd.params.len;
var arg_idx: usize = 1;
for (gen_fd.params[1..]) |p| {
if (isTypeParamDecl(&p, gen_fd.type_params)) {
if (types_explicit) arg_idx += 1;
continue;
}
if (arg_idx < method_args.items.len) {
gvalue_args.append(self.alloc, method_args.items[arg_idx]) catch unreachable;
}
arg_idx += 1;
}
self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty);
self.coerceCallArgs(gvalue_args.items, gparams);
return self.builder.call(gfid, gvalue_args.items, gret_ty);
}
}
}
// Try non-generic qualified method
if (self.fn_ast_map.get(qualified)) |fd| {
if (!self.lowered_functions.contains(qualified)) {
@@ -5134,6 +5272,49 @@ pub const Lowering = struct {
}
}
/// Emit `context.allocator.alloc(size)` dispatch — used by internal
/// compiler-driven heap copies (e.g. the `xx value` protocol-erasure
/// path in `buildProtocolValue`). Routes through whatever allocator is
/// currently installed in `context`, so a surrounding
/// `push Context.{ allocator = my_alloc, ... }` actually backs every
/// allocation including the ones the compiler inserts.
///
/// Falls back to `.heap_alloc` (libc malloc) only when the `context`
/// global hasn't been registered (programs that don't `#import
/// "modules/std.sx"`). All standard sx code imports std.sx via
/// allocators.sx, so the fallback exists strictly for the bootstrapping
/// edge case.
fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref {
const ctx_gi = self.global_names.get("context") orelse {
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
};
const ctx_ty_info = self.module.types.get(ctx_gi.ty);
if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) {
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
}
const allocator_ty = ctx_ty_info.@"struct".fields[0].ty;
const ctx = self.builder.emit(.{ .global_get = ctx_gi.id }, ctx_gi.ty);
const allocator = self.builder.structGet(ctx, 0, allocator_ty);
// #inline Allocator protocol layout: { ctx, alloc_fn_ptr, dealloc_fn_ptr }.
// field 0 = receiver ctx, field 1 = alloc fn-ptr.
const alloc_ctx = self.builder.structGet(allocator, 0, void_ptr_ty);
const fn_ptr = self.builder.structGet(allocator, 1, void_ptr_ty);
const args = self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable;
return self.builder.emit(.{ .call_indirect = .{
.callee = fn_ptr,
.args = args,
} }, void_ptr_ty);
}
/// Emit a call to a foreign-declared function looked up by name.
/// Used for the compiler-internal byte-copy in the protocol-erasure
/// heap path and the closure env-copy path, both of which need
/// libc `memcpy` after the `#builtin` form was dropped.
fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref {
const fid = self.resolveFuncByName(name) orelse @panic("foreign symbol missing — std.sx not imported?");
return self.builder.call(fid, args, ret_ty);
}
/// Pattern-match `context.allocator.alloc(size)` → heap_alloc,
/// `context.allocator.dealloc(ptr)` → heap_free.
fn matchContextAllocCall(self: *Lowering, fa: ast.FieldAccess, call_args: []const Ref) ?Ref {
@@ -5177,11 +5358,8 @@ pub const Lowering = struct {
.{ "cos", inst_mod.BuiltinId.cos },
.{ "floor", inst_mod.BuiltinId.floor },
.{ "size_of", inst_mod.BuiltinId.size_of },
.{ "align_of", inst_mod.BuiltinId.align_of },
.{ "cast", inst_mod.BuiltinId.cast },
.{ "malloc", inst_mod.BuiltinId.malloc },
.{ "free", inst_mod.BuiltinId.free },
.{ "memcpy", inst_mod.BuiltinId.memcpy },
.{ "memset", inst_mod.BuiltinId.memset },
};
inline for (builtins) |entry| {
if (std.mem.eql(u8, name, entry[0])) return entry[1];
@@ -5349,11 +5527,7 @@ pub const Lowering = struct {
const env_byte_size_inner = self.computeEnvSize(capture_list);
const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64);
// memcpy(local_alloca, env_param, size)
const cp_args = self.alloc.dupe(Ref, &.{ env_local, env_param_ref, env_size_val }) catch unreachable;
_ = self.builder.emit(.{ .call_builtin = .{
.builtin = inst_mod.BuiltinId.memcpy,
.args = cp_args,
} }, self.module.types.ptrTo(.void));
_ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void));
for (capture_list, 0..) |cap, i| {
// GEP into env struct to get field pointer
@@ -5440,11 +5614,7 @@ pub const Lowering = struct {
const ptr_void = self.module.types.ptrTo(.void);
const env_heap = self.builder.emit(.{ .heap_alloc = .{ .operand = env_size } }, ptr_void);
// memcpy(heap, stack_alloca, size)
const args = self.alloc.dupe(Ref, &.{ env_heap, env_local, env_size }) catch unreachable;
_ = self.builder.emit(.{ .call_builtin = .{
.builtin = inst_mod.BuiltinId.memcpy,
.args = args,
} }, ptr_void);
_ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void);
return self.builder.closureCreate(func_id, env_heap, closure_ty);
} else {
@@ -6369,31 +6539,26 @@ pub const Lowering = struct {
// ── Generic monomorphization ──────────────────────────────────
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
// Infer type param bindings from call arguments
/// Build `tp.name -> TypeId` bindings for a generic call.
/// `args_ast` must be parallel to `fd.params`; for dot-calls the caller
/// prepends the receiver's AST node so positions align with `fd.params[0] = self`.
/// Caller owns the returned map and must call `.deinit()`.
fn buildTypeBindings(
self: *Lowering,
fd: *const ast.FnDecl,
args_ast: []const *const Node,
) std.StringHashMap(TypeId) {
var bindings = std.StringHashMap(TypeId).init(self.alloc);
defer bindings.deinit();
// Determine if type args are passed explicitly:
// If call_node.args.len == fd.params.len, the caller passed type args explicitly
// (e.g., are_equal(Point, p1, p2)). Otherwise, types are inferred from value args
// (e.g., are_equal(p1, p2)).
const types_passed_explicitly = call_node.args.len == fd.params.len;
const types_passed_explicitly = args_ast.len == fd.params.len;
for (fd.type_params) |tp| {
var found = false;
// Strategy 1: Direct type param declaration ($T: Type)
// The param whose name matches the type param IS the declaration.
// The call arg at that position is a type expression — resolve it directly.
// Only applies when type args are passed explicitly in the call.
// Strategy 1: explicit — the param whose name matches `tp.name` IS
// the `$T: Type` declaration; the arg at that position is a type expression.
if (types_passed_explicitly) {
for (fd.params, 0..) |param, pi| {
if (std.mem.eql(u8, param.name, tp.name)) {
// This param IS the type param declaration
if (pi < call_node.args.len) {
const ty = self.resolveTypeArg(call_node.args[pi]);
if (pi < args_ast.len and type_bridge.isTypeShapedAstNode(args_ast[pi], &self.module.types)) {
const ty = self.resolveTypeArg(args_ast[pi]);
bindings.put(tp.name, ty) catch {};
found = true;
}
@@ -6402,11 +6567,8 @@ pub const Lowering = struct {
}
}
if (found) continue;
// Strategy 2: Infer from params that USE the type param (e.g., a: $T, b: T, items: []$T)
// Check ALL params whose type matches the type param name, pick widest type.
// When types are inferred (not explicit), use a separate arg index that
// skips type param declarations to correctly map params to call args.
// Strategy 2: infer from value params that USE the type param
// (e.g. a: $T, b: T, items: []$T). Pick widest type across matches.
var inferred_ty: ?TypeId = null;
var s2_arg_idx: usize = 0;
for (fd.params) |param| {
@@ -6420,8 +6582,8 @@ pub const Lowering = struct {
}
const matched = self.matchTypeParam(param.type_expr, tp.name);
if (matched) {
if (s2_arg_idx < call_node.args.len) {
const arg_ty = self.inferExprType(call_node.args[s2_arg_idx]);
if (s2_arg_idx < args_ast.len) {
const arg_ty = self.inferExprType(args_ast[s2_arg_idx]);
const extracted = self.extractTypeParam(param.type_expr, arg_ty, tp.name);
if (extracted) |ety| {
if (inferred_ty) |prev| {
@@ -6441,8 +6603,17 @@ pub const Lowering = struct {
bindings.put(tp.name, ty) catch {};
}
}
return bindings;
}
// Build mangled name: "func_name__Type1_Type2"
/// Mangle a generic call site into "base__Type1_Type2".
/// Returns a heap-allocated string owned by self.alloc.
fn mangleGenericName(
self: *Lowering,
base_name: []const u8,
fd: *const ast.FnDecl,
bindings: *const std.StringHashMap(TypeId),
) []const u8 {
var mangled_buf: [256]u8 = undefined;
var mangled_len: usize = 0;
for (base_name) |ch| {
@@ -6452,14 +6623,12 @@ pub const Lowering = struct {
}
}
for (fd.type_params) |tp| {
// Append separator
for ("__") |ch| {
if (mangled_len < mangled_buf.len) {
mangled_buf[mangled_len] = ch;
mangled_len += 1;
}
}
// Append type name
const ty = bindings.get(tp.name) orelse .s64;
const type_name_str = self.mangleTypeName(ty);
for (type_name_str) |ch| {
@@ -6469,27 +6638,31 @@ pub const Lowering = struct {
}
}
}
const mangled_name = mangled_buf[0..mangled_len];
return self.alloc.dupe(u8, mangled_buf[0..mangled_len]) catch base_name;
}
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
var bindings = self.buildTypeBindings(fd, call_node.args);
defer bindings.deinit();
const types_passed_explicitly = call_node.args.len == fd.params.len;
const mangled_name = self.mangleGenericName(base_name, fd, &bindings);
// Check cache
if (!self.lowered_functions.contains(mangled_name)) {
// Monomorphize: create a new function with the mangled name and lower with type bindings
self.monomorphizeFunction(fd, mangled_name, &bindings);
}
// Resolve the monomorphized function and call it (stripping type args)
if (self.resolveFuncByName(mangled_name)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
// Build value-only args (skip type param declaration args)
// Use separate index for lowered_args since type params don't consume call args
var value_args = std.ArrayList(Ref).empty;
defer value_args.deinit(self.alloc);
var arg_idx: usize = 0;
for (fd.params) |p| {
if (isTypeParamDecl(&p, fd.type_params)) {
// Only skip in lowered_args if types were passed explicitly in the call
if (types_passed_explicitly) arg_idx += 1;
continue;
}
@@ -6894,6 +7067,11 @@ pub const Lowering = struct {
const size: i64 = @intCast(self.typeSizeBytes(ty));
return self.builder.constInt(size, .s64);
}
if (std.mem.eql(u8, name, "align_of")) {
const ty = self.resolveTypeArg(c.args[0]);
const a: i64 = @intCast(self.module.types.typeAlignBytes(ty));
return self.builder.constInt(a, .s64);
}
if (std.mem.eql(u8, name, "field_count")) {
// field_count(T) → const_int(N)
const ty = self.resolveTypeArg(c.args[0]);
@@ -7019,9 +7197,13 @@ pub const Lowering = struct {
if (self.type_bindings) |tb| {
if (tb.get(id.name)) |ty| return ty;
}
// Try as a named type by name (resolveAstType doesn't handle .identifier)
if (self.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
const name_id = self.module.types.internString(id.name);
return self.module.types.findByName(name_id) orelse .s64;
if (self.module.types.findByName(name_id)) |t| return t;
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "unresolved type: '{s}'", .{id.name});
}
return .void;
},
.type_expr => |te| {
if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
@@ -7031,6 +7213,14 @@ pub const Lowering = struct {
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
return self.resolveTypeCallWithBindings(&cl);
},
.pointer_type_expr,
.many_pointer_type_expr,
.array_type_expr,
.slice_type_expr,
.optional_type_expr,
.function_type_expr,
.tuple_literal,
=> return type_bridge.resolveAstType(node, &self.module.types),
else => return .s64,
}
}
@@ -7498,6 +7688,41 @@ pub const Lowering = struct {
// skipping the first param (self) since it's prepended later.
if (c.callee.data == .field_access) {
const fa = c.callee.data.field_access;
// Namespace/static call: `Type.method(args)` where `Type` is a type
// identifier (not a value in scope). Args correspond to ALL params
// — no self prepend — so target_type for arg lowering must include
// the leading param. Skipping it would lose the protocol context
// for `xx ptr` inline-cast args.
if (fa.object.data == .identifier) {
const obj_name = fa.object.data.identifier.name;
const is_value = blk: {
if (self.scope) |scope| {
if (scope.lookup(obj_name) != null) break :blk true;
}
if (self.global_names.contains(obj_name)) break :blk true;
break :blk false;
};
if (!is_value) {
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch return &.{};
if (self.resolveFuncByName(qualified)) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
var types_list = std.ArrayList(TypeId).empty;
for (func.params) |p| {
types_list.append(self.alloc, p.ty) catch unreachable;
}
return types_list.items;
}
if (self.fn_ast_map.get(qualified)) |fd| {
var types_list = std.ArrayList(TypeId).empty;
for (fd.params) |p| {
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable;
}
return types_list.items;
}
}
}
const obj_ty = self.inferExprType(fa.object);
// Protocol-typed receiver: look up the method on the protocol decl. The
// protocol's ProtocolMethodInfo.param_types already excludes self.
@@ -8511,9 +8736,11 @@ pub const Lowering = struct {
};
ptypes.append(self.alloc, pty) catch unreachable;
}
var ret_is_self = false;
const ret = if (method.return_type) |rt| blk: {
if (rt.data == .type_expr) {
if (std.mem.eql(u8, rt.data.type_expr.name, "Self")) {
ret_is_self = true;
break :blk void_ptr_ty;
}
if (self.type_alias_map.get(rt.data.type_expr.name)) |aliased| {
@@ -8526,6 +8753,7 @@ pub const Lowering = struct {
.name = method.name,
.param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable,
.ret_type = ret,
.ret_is_self = ret_is_self,
}) catch unreachable;
}
self.protocol_decl_map.put(pd.name, .{
@@ -8799,6 +9027,54 @@ pub const Lowering = struct {
return owned;
}
/// Emit the process-wide default Context as an LLVM static constant.
///
/// @__sx_default_context = internal constant %Context {
/// %Allocator { ptr null,
/// ptr @__thunk_CAllocator_Allocator_alloc,
/// ptr @__thunk_CAllocator_Allocator_dealloc },
/// ptr null
/// }
///
/// Used by FFI inbound wrappers (Step 4) and the interp's default-
/// context call entry (Step 7). Only emitted when the program imports
/// `std.sx` — without that, Context / Allocator / CAllocator aren't
/// registered and the global has no purpose.
fn emitDefaultContextGlobal(self: *Lowering) void {
const tbl = &self.module.types;
const ctx_name_id = tbl.internString("Context");
const ctx_ty = tbl.findByName(ctx_name_id) orelse return;
if (tbl.findByName(tbl.internString("Allocator")) == null) return;
if (tbl.findByName(tbl.internString("CAllocator")) == null) return;
// Force the CAllocator → Allocator thunks to exist so we can
// reference them by FuncId in the static initializer.
const thunks = self.getOrCreateThunks("Allocator", "CAllocator");
if (thunks.len < 2) return;
// Inline Allocator value: { ctx: *void, alloc_fn: *void, dealloc_fn: *void }
// CAllocator is stateless, so ctx is null.
const alloc_fields = self.alloc.alloc(inst_mod.ConstantValue, 3) catch return;
alloc_fields[0] = .null_val;
alloc_fields[1] = .{ .func_ref = thunks[0] };
alloc_fields[2] = .{ .func_ref = thunks[1] };
// Context value: { allocator: Allocator, data: *void }
const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 2) catch return;
ctx_fields[0] = .{ .aggregate = alloc_fields };
ctx_fields[1] = .null_val;
const global_name = "__sx_default_context";
const global_name_id = tbl.internString(global_name);
const gid = self.module.addGlobal(.{
.name = global_name_id,
.ty = ctx_ty,
.init_val = .{ .aggregate = ctx_fields },
.is_const = true,
});
self.global_names.put(global_name, .{ .id = gid, .ty = ctx_ty }) catch {};
}
/// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret
/// The thunk calls ConcreteType.method(ctx, args...).
fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId {
@@ -8925,12 +9201,8 @@ pub const Lowering = struct {
if (heap_copy) {
const concrete_size = self.module.types.typeSizeBytes(concrete_ty);
const size_ref = self.builder.constInt(@intCast(concrete_size), .s64);
const heap_ptr = self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
const memcpy_args = self.alloc.dupe(Ref, &.{ heap_ptr, concrete_ptr, size_ref }) catch unreachable;
_ = self.builder.emit(.{ .call_builtin = .{
.builtin = inst_mod.BuiltinId.memcpy,
.args = memcpy_args,
} }, void_ptr_ty);
const heap_ptr = self.allocViaContext(size_ref, void_ptr_ty);
_ = self.callForeign("memcpy", &.{ heap_ptr, concrete_ptr, size_ref }, void_ptr_ty);
ctx_ptr = heap_ptr;
}
@@ -9055,12 +9327,11 @@ pub const Lowering = struct {
const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable;
const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type);
// If protocol method returns *void (Self) and the caller expects a value type,
// unbox: load the concrete value from the returned pointer. Real pointer
// returns (declared `-> *T` for non-Self T) are NOT auto-loaded — the
// pointee may be a single byte and reading `sizeof(target)` past it
// segfaults. Self is encoded as `*void`, so test against that exact type.
if (mi.ret_type == void_ptr) {
// If the protocol method was declared `-> Self` (encoded here as *void)
// and the caller expects a value type, unbox: load the concrete value
// from the returned pointer. A literal `-> *void` return is NOT
// auto-loaded — it's a real pointer whose pointee size we don't know.
if (mi.ret_is_self) {
if (self.target_type) |target| {
const target_info = self.module.types.get(target);
if (target_info != .pointer) {
@@ -9149,7 +9420,7 @@ pub const Lowering = struct {
}
break :blk TypeId.f64;
},
.size_of, .malloc => .s64,
.size_of, .align_of => .s64,
.cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .s64,
else => .s64,
};
@@ -9376,15 +9647,17 @@ pub const Lowering = struct {
defer tmp_bindings.deinit();
for (fd.type_params) |tp| {
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name.
// Only fires when the caller actually supplied a type expression at
// that position; otherwise fall through to value-based inference.
var found = false;
for (fd.params, 0..) |param, pi| {
if (std.mem.eql(u8, param.name, tp.name)) {
if (pi < c.args.len) {
if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.module.types)) {
const ty = self.resolveTypeArg(c.args[pi]);
tmp_bindings.put(tp.name, ty) catch {};
found = true;
}
found = true;
break;
}
}
@@ -9482,6 +9755,21 @@ pub const Lowering = struct {
return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty);
}
// Protocol → pointer: recover the typed ctx pointer (field 0).
// The protocol value is `{ ctx, fn1, fn2, ... }` (inline) or
// `{ ctx, vtable_ptr }` — either way, ctx lives at field 0.
if (self.getProtocolInfo(src_ty)) |_| {
if (!dst_ty.isBuiltin()) {
const dst_info = self.module.types.get(dst_ty);
if (dst_info == .pointer) {
const void_ptr_ty = self.module.types.ptrTo(.void);
const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = operand, .field_index = 0 } }, void_ptr_ty);
if (dst_ty == void_ptr_ty) return ctx_ref;
return self.builder.emit(.{ .bitcast = .{ .operand = ctx_ref, .from = void_ptr_ty, .to = dst_ty } }, dst_ty);
}
}
}
const result = self.coerceToType(operand, src_ty, dst_ty);
// User-space fallback via `impl Into(Target) for Source`. Only fires
@@ -9900,7 +10188,26 @@ pub const Lowering = struct {
fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref {
if (self.diagnostics) |diags| {
diags.addFmt(.err, span, "unresolved: '{s}'", .{name});
// The literal message carries the lowering's `current_source_file`
// and enclosing function name. The diagnostic renderer's
// `source_file` -> `file:line:col` prefix can drift when a span is
// offset into one source but the diagnostic falls back to another
// (e.g. synthetic AST nodes inserted from `#insert` take their
// span from the call site, not from the string being inserted).
// Embedding the file + function in the message means a
// misattributed span can never hide WHERE the lookup actually
// failed. Setting SX_TRACE_UNRESOLVED=1 also dumps a Zig stack
// trace at the emit site to surface the calling lowering path.
const sf = self.current_source_file orelse "<unknown>";
const fn_name: []const u8 = if (self.builder.func) |fid|
self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name)
else
"<top-level>";
if (std.c.getenv("SX_TRACE_UNRESOLVED") != null) {
std.debug.print("\n== unresolved '{s}' (in {s} fn {s}) ==\n", .{ name, sf, fn_name });
std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() });
}
diags.addFmt(.err, span, "unresolved '{s}' (in {s} fn {s})", .{ name, sf, fn_name });
}
return self.emitPlaceholder(name);
}

View File

@@ -516,6 +516,7 @@ fn writeConstant(val: ConstantValue, writer: Writer) !void {
.zeroinit => try writer.writeAll("zeroinit"),
.aggregate => try writer.writeAll("{...}"),
.vtable => try writer.writeAll("vtable{...}"),
.func_ref => |fid| try writer.print("func_ref(#{d})", .{fid.index()}),
}
}

View File

@@ -26,6 +26,7 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
.function_type_expr => |ft| resolveFunctionType(&ft, table),
.closure_type_expr => |ct| resolveClosureType(&ct, table),
.tuple_type_expr => |tt| resolveTupleType(&tt, table),
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table),
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table),
.inferred_type => .s64, // inferred — default until we have type inference
// Inline type declarations (used as field types)
@@ -299,6 +300,62 @@ fn resolveTupleType(tt: *const ast.TupleTypeExpr, table: *TypeTable) TypeId {
} });
}
// Treat a tuple value literal as the corresponding tuple TYPE — valid only when
// every element is itself a type expression. Non-type elements report a clear
// diagnostic and degrade to .s64 for that slot (which the snapshot will catch).
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable) TypeId {
const alloc = table.alloc;
var field_ids = std.ArrayList(TypeId).empty;
var name_ids_list = std.ArrayList(StringId).empty;
var any_named = false;
for (tl.elements) |el| {
if (!isTypeShapedAstNode(el.value, table)) {
std.debug.print("type_bridge: tuple literal element is not a type (tag={s}) — cannot use as tuple type\n", .{@tagName(el.value.data)});
field_ids.append(alloc, .s64) catch unreachable;
} else {
field_ids.append(alloc, resolveAstType(el.value, table)) catch unreachable;
}
if (el.name) |n| {
any_named = true;
name_ids_list.append(alloc, table.internString(n)) catch unreachable;
} else {
name_ids_list.append(alloc, table.internString("")) catch unreachable;
}
}
const names: ?[]const StringId = if (any_named) name_ids_list.items else null;
return table.intern(.{ .tuple = .{
.fields = field_ids.items,
.names = names,
} });
}
// Returns true when this AST node, on its own, denotes a type rather than a
// value. Used to guard tuple-literal-as-type reinterpretation: a tuple literal
// becomes a tuple type only when every element is a type.
pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool {
return switch (node.data) {
.type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
.array_type_expr,
.slice_type_expr,
.optional_type_expr,
.function_type_expr,
.closure_type_expr,
.tuple_type_expr,
.parameterized_type_expr,
=> true,
.identifier => |id| table.findByName(table.internString(id.name)) != null,
.tuple_literal => |tl| blk: {
for (tl.elements) |el| {
if (!isTypeShapedAstNode(el.value, table)) break :blk false;
}
break :blk true;
},
else => false,
};
}
fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable) TypeId {
// Strip module prefix (e.g. "std.Vector" → "Vector")
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;

View File

@@ -505,7 +505,8 @@ pub const Server = struct {
.{ .label = "field_count", .detail = "($T: Type) -> s32" },
.{ .label = "field_name", .detail = "($T: Type, idx: s32) -> string" },
.{ .label = "field_value", .detail = "(s: $T, idx: s32) -> Any" },
.{ .label = "size_of", .detail = "($T: Type) -> s32" },
.{ .label = "size_of", .detail = "($T: Type) -> s64" },
.{ .label = "align_of", .detail = "($T: Type) -> s64" },
.{ .label = "cast", .detail = "(Type) expr — prefix type cast" },
.{ .label = "malloc", .detail = "(size: s64) -> *void" },
.{ .label = "free", .detail = "(ptr: *void) -> void" },
@@ -974,7 +975,8 @@ pub const Server = struct {
.{ .name = "field_count", .label = "field_count($T: Type) -> s32", .params = &.{"$T: Type"} },
.{ .name = "field_name", .label = "field_name($T: Type, idx: s32) -> string", .params = &.{ "$T: Type", "idx: s32" } },
.{ .name = "field_value", .label = "field_value(s: $T, idx: s32) -> Any", .params = &.{ "s: $T", "idx: s32" } },
.{ .name = "size_of", .label = "size_of($T: Type) -> s32", .params = &.{"$T: Type"} },
.{ .name = "size_of", .label = "size_of($T: Type) -> s64", .params = &.{"$T: Type"} },
.{ .name = "align_of", .label = "align_of($T: Type) -> s64", .params = &.{"$T: Type"} },
.{ .name = "cast", .label = "cast(Type) expr", .params = &.{"Type"} },
.{ .name = "malloc", .label = "malloc(size: s64) -> *void", .params = &.{"size: s64"} },
.{ .name = "free", .label = "free(ptr: *void) -> void", .params = &.{"ptr: *void"} },

View File

@@ -2240,6 +2240,10 @@ pub const Parser = struct {
if (self.isLambda()) {
return self.parseLambda();
}
// Function-type literal: (T1, T2) -> R (no body — isLambda would have caught a body)
if (self.isFunctionTypeExprAtLParen()) {
return try self.parseTypeExpr();
}
self.advance(); // skip '('
// Check for named tuple: (name: expr, ...)
@@ -2312,8 +2316,7 @@ pub const Parser = struct {
null;
return try self.createNode(start, .{ .return_stmt = .{ .value = value } });
},
.l_bracket => {
// Type expression in expression position: []T.[...] or [N]T.[...]
.l_bracket, .star, .question => {
return try self.parseTypeExpr();
},
.l_brace => {
@@ -2728,6 +2731,32 @@ pub const Parser = struct {
return self.current.tag;
}
/// Returns true when the current `(` opens a function-type literal `(T1, T2) -> R`
/// rather than a tuple/grouping/lambda. Only meaningful after `isLambda` has
/// returned false — at that point a trailing `->` after the matching `)` can
/// only be a function type, since any body (`=>` or `{`) would have made it
/// a lambda.
fn isFunctionTypeExprAtLParen(self: *Parser) bool {
const saved_lexer = self.lexer;
const saved_current = self.current;
const saved_prev_end = self.prev_end;
defer {
self.lexer = saved_lexer;
self.current = saved_current;
self.prev_end = saved_prev_end;
}
self.advance(); // skip '('
var depth: u32 = 1;
while (depth > 0 and self.current.tag != .eof) {
if (self.current.tag == .l_paren) depth += 1;
if (self.current.tag == .r_paren) depth -= 1;
if (depth > 0) self.advance();
}
if (self.current.tag != .r_paren) return false;
self.advance(); // skip ')'
return self.current.tag == .arrow;
}
fn isLambda(self: *Parser) bool {
const saved_lexer = self.lexer;
const saved_current = self.current;
@@ -2845,7 +2874,55 @@ pub const Parser = struct {
// ends with `;` directly after the param list — recognise it as a
// function def (not a constant) so it goes through parseFnDecl.
if (self.struct_default_compiler and tag == .semicolon) return true;
return tag == .l_brace or tag == .arrow or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow or tag == .kw_callconv;
// `(T1, T2) -> R` without a trailing body (`{`, `=>`, or a foreign/
// builtin marker) is a function-type literal, not a function def.
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;
}
fn hasFnBodyAfterArrow(self: *Parser) bool {
const saved_lexer = self.lexer;
const saved_current = self.current;
const saved_prev_end = self.prev_end;
defer {
self.lexer = saved_lexer;
self.current = saved_current;
self.prev_end = saved_prev_end;
}
self.advance(); // skip '('
var depth: u32 = 1;
while (depth > 0 and self.current.tag != .eof) {
if (self.current.tag == .l_paren) depth += 1;
if (self.current.tag == .r_paren) depth -= 1;
if (depth > 0) self.advance();
}
if (self.current.tag != .r_paren) return false;
self.advance(); // skip ')'
if (self.current.tag != .arrow) return false;
self.advance(); // skip '->'
while (self.current.tag != .eof) {
if (self.current.tag == .fat_arrow) 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 == .kw_callconv) return true;
// Inside a `struct #compiler` block, a `(...) -> Ret;` ending
// with `;` after the return type is a `#compiler` method
// declaration (body implicit). Outside that context, the same
// shape is a function-type alias (no body) and falls through to
// const-decl parsing.
if (self.struct_default_compiler and self.current.tag == .semicolon) return true;
if (self.current.tag == .identifier or self.current.tag.isTypeKeyword() or
self.current.tag == .dot or self.current.tag == .dollar or
self.current.tag == .l_bracket or self.current.tag == .r_bracket or
self.current.tag == .l_paren or self.current.tag == .r_paren or
self.current.tag == .comma or self.current.tag == .int_literal or
self.current.tag == .star or self.current.tag == .question or
self.current.tag == .colon or self.current.tag == .arrow)
{
self.advance();
} else break;
}
return false;
}
fn parseOptionalCallConv(self: *Parser) anyerror!ast.CallingConvention {

View File

@@ -655,7 +655,7 @@ pub const Analyzer = struct {
}
// Built-in names that aren't declared in source
const builtins = [_][]const u8{ "io", "true", "false", "cast", "closure", "out", "size_of", "malloc", "free", "memcpy", "memset" };
const builtins = [_][]const u8{ "io", "true", "false", "cast", "closure", "out", "size_of", "align_of", "malloc", "free", "memcpy", "memset" };
for (builtins) |b| {
if (std.mem.eql(u8, name, b)) return;
}

View File

@@ -1,4 +1,4 @@
a 0 : Foo{a: 0, b: 42, c: 176, d: 17}
a 0 : Foo{a: 0, b: 42, c: 92, d: 17}
a 1 : Foo{a: 1, b: 42, c: 8, d: 17}
b: Foo{a: 1, b: 1, c: 101, d: 1}
Pack{a: 1, b: 0, c: 3, d: 5, f: 9, v: 100, x: 3.500000}

View File

@@ -12,5 +12,3 @@ sqrt(9): 3.000000
4
16
8
8
8

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
Box.init (named-var): ok
make_box (inline-xx): ok
Box.init (inline-xx): ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,4 @@
recovered == gpa? true
alloc count after first alloc: 1
recovered2 == gpa? true
alloc count after dealloc: 0

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,3 @@
/Users/agra/projects/sx/examples/127-import-non-transitive.sx:15:37: error: 'c_only_fn' is not visible; #import the module that declares it
/Users/agra/projects/sx/examples/127-import-non-transitive.sx:16:40: error: 'c_only_const' is not visible; #import the module that declares it
/Users/agra/projects/sx/examples/127-import-non-transitive.sx:16:40: error: unresolved 'c_only_const' (in /Users/agra/projects/sx/examples/127-import-non-transitive.sx fn main)

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
b=42

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,5 @@
plain: 7
sized s32: 4
sized s64: 8
taking explicit: 42
taking inferred: 99

View File

@@ -0,0 +1 @@
Tracer.count = 1

View File

@@ -228,6 +228,10 @@ sqrt-f64: 4.000000
sizeof-s32: 4
sizeof-f64: 8
sizeof-struct: 8
alignof-u8: 1
alignof-s32: 4
alignof-s64: 8
alignof-struct: 4
typeof: int
typeof-float: float
typeof-string: string
@@ -385,12 +389,12 @@ bytes len: 3
--- allocators ---
gpa allocs: 2
gpa final: 0
arena chunks: 1
arena overflow: 2
arena chunks: 2
arena overflow: 3
arena a1: 42
arena a3: 99
arena reset idx: 0
arena reset gpa: 1
arena reset gpa: 2
arena deinit: 0
buf pos: 48
buf overflow: 0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
direct: null? false
protocol: null? false
alloc_count: 2

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [5 x i8] c"noop\00", align 1
@str.1 = private unnamed_addr constant [4 x i8] c"()V\00", align 1
@SX_JNI_CLS_noop____V = internal global ptr null
@@ -15,14 +16,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -37,8 +62,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -62,7 +87,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -77,7 +102,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -88,6 +113,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -156,7 +196,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -164,7 +204,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -189,7 +229,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -310,6 +350,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [9 x i8] c"getCount\00", align 1
@str.1 = private unnamed_addr constant [4 x i8] c"()I\00", align 1
@SX_JNI_CLS_getCount____I = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -285,6 +325,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [18 x i8] c"currentTimeMillis\00", align 1
@str.1 = private unnamed_addr constant [4 x i8] c"()J\00", align 1
@SX_JNI_CLS_currentTimeMillis____J = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -285,6 +325,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [9 x i8] c"getValue\00", align 1
@str.1 = private unnamed_addr constant [4 x i8] c"()D\00", align 1
@SX_JNI_CLS_getValue____D = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -285,6 +325,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [8 x i8] c"isShown\00", align 1
@str.1 = private unnamed_addr constant [4 x i8] c"()Z\00", align 1
@SX_JNI_CLS_isShown____Z = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -285,6 +325,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [10 x i8] c"getWindow\00", align 1
@str.1 = private unnamed_addr constant [24 x i8] c"()Landroid/view/Window;\00", align 1
@SX_JNI_CLS_getWindow____Landroid_view_Window_ = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -285,6 +325,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [4 x i8] c"max\00", align 1
@str.1 = private unnamed_addr constant [6 x i8] c"(II)I\00", align 1
@SX_JNI_CLS_max___II_I = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -282,6 +322,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [10 x i8] c"getWindow\00", align 1
@str.1 = private unnamed_addr constant [21 x i8] c"()Ljava/lang/Object;\00", align 1
@SX_JNI_CLS_getWindow____Ljava_lang_Object_ = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -285,6 +325,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -1,6 +1,7 @@
@context = internal global { { ptr, ptr, ptr }, ptr } zeroinitializer
@g_should_call = internal global i1 false
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@str = private unnamed_addr constant [5 x i8] c"noop\00", align 1
@str.1 = private unnamed_addr constant [4 x i8] c"()V\00", align 1
@SX_JNI_CLS_noop____V = internal global ptr null
@@ -13,14 +14,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -35,8 +60,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -60,7 +85,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -75,7 +100,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -86,6 +111,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -154,7 +194,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -162,7 +202,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -187,7 +227,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -283,6 +323,20 @@ if.merge.1: ; preds = %if.then.0, %entry
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

View File

@@ -3,6 +3,7 @@
@OS = internal global i64 0
@ARCH = internal global i64 0
@POINTER_SIZE = internal global i64 8
@__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null }
@OBJC_SELECTOR_REFERENCES_init = internal global ptr null
@OBJC_SELECTOR_REFERENCES_release = internal global ptr null
@str = private unnamed_addr constant [4 x i8] c"ok\0A\00", align 1
@@ -16,14 +17,38 @@ declare void @out(ptr) #0
declare ptr @malloc(i64)
declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare ptr @memset(ptr, i32, i64)
declare void @free(ptr)
; Function Attrs: nounwind
define internal ptr @CAllocator.alloc(ptr %0, i64 %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca i64, align 8
store i64 %1, ptr %allocaN, align 8
%load = load i64, ptr %allocaN, align 8
%call = call ptr @malloc(i64 %load)
ret ptr %call
}
; Function Attrs: nounwind
declare void @GPA.create(ptr sret({ ptr, ptr, ptr }), ptr) #0
define internal void @CAllocator.dealloc(ptr %0, ptr %1) #0 {
entry:
%alloca = alloca ptr, align 8
store ptr %0, ptr %alloca, align 8
%allocaN = alloca ptr, align 8
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %allocaN, align 8
call void @free(ptr %load)
ret void
}
; Function Attrs: nounwind
declare ptr @GPA.init() #0
; Function Attrs: nounwind
define internal ptr @GPA.alloc(ptr %0, i64 %1) #0 {
@@ -38,8 +63,8 @@ entry:
%add = add i64 %loadN, 1
store i64 %add, ptr %gep, align 8
%loadN = load i64, ptr %allocaN, align 8
%malloc = call ptr @malloc(i64 %loadN)
ret ptr %malloc
%call = call ptr @malloc(i64 %loadN)
ret ptr %call
}
; Function Attrs: nounwind
@@ -63,7 +88,7 @@ entry:
declare void @Arena.add_chunk(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @Arena.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @Arena.reset(ptr) #0
@@ -78,7 +103,7 @@ declare ptr @Arena.alloc(ptr, i64) #0
declare void @Arena.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare void @BufAlloc.create(ptr sret({ ptr, ptr, ptr }), ptr, ptr, i64) #0
declare ptr @BufAlloc.init(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.reset(ptr) #0
@@ -89,6 +114,21 @@ declare ptr @BufAlloc.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @BufAlloc.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.init(ptr) #0
; Function Attrs: nounwind
declare i64 @TrackingAllocator.leak_count(ptr) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.report(ptr) #0
; Function Attrs: nounwind
declare ptr @TrackingAllocator.alloc(ptr, i64) #0
; Function Attrs: nounwind
declare void @TrackingAllocator.dealloc(ptr, ptr) #0
; Function Attrs: nounwind
define internal { ptr, i64 } @cstring(i64 %0) #0 {
entry:
@@ -157,7 +197,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %alloca, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%2 = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %dptrN, i64 %loadN)
%loadN = load i64, ptr %allocaN, align 8
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
@@ -165,7 +205,7 @@ entry:
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%dptrN = extractvalue { ptr, i64 } %loadN, 0
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%callN = call ptr @memcpy(ptr %igp.ptr, ptr %dptrN, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -190,7 +230,7 @@ entry:
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr i8, ptr %igp.data, i64 %loadN
%loadN = load i64, ptr %allocaN, align 8
%3 = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%callN = call ptr @memcpy(ptr %dptr, ptr %igp.ptr, i64 %loadN)
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
ret { ptr, i64 } %loadN
}
@@ -359,6 +399,20 @@ entry:
ret i32 0
}
; Function Attrs: nounwind
define internal ptr @__thunk_CAllocator_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:
%call = call ptr @CAllocator.alloc(ptr %0, i64 %1)
ret ptr %call
}
; Function Attrs: nounwind
define internal void @__thunk_CAllocator_Allocator_dealloc(ptr %0, ptr %1) #0 {
entry:
call void @CAllocator.dealloc(ptr %0, ptr %1)
ret void
}
; Function Attrs: nounwind
define internal ptr @__thunk_GPA_Allocator_alloc(ptr %0, i64 %1) #0 {
entry:

File diff suppressed because it is too large Load Diff

53
tools/scratch.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
# tools/scratch.sh
#
# Quick interp/codegen parity test for a snippet.
# Reads sx source from stdin, runs it under both `sx run` (interp)
# and `sx build` + spawn (codegen), diffs the stdout output.
#
# Usage:
# echo 'main :: () { print("hello\n"); }' | tools/scratch.sh
# cat my_test.sx | tools/scratch.sh
set -u
SX="/Users/agra/projects/sx/zig-out/bin/sx"
SRC=/tmp/scratch.sx
BIN=/tmp/scratch-bin
RUN_LOG=/tmp/scratch-run.log
BUILD_LOG=/tmp/scratch-build.log
cat > "$SRC"
# Interp path
"$SX" run "$SRC" > "$RUN_LOG" 2>&1
RUN_EXIT=$?
# Codegen path
if "$SX" build "$SRC" -o "$BIN" >> "$BUILD_LOG" 2>&1; then
"$BIN" > "$BUILD_LOG" 2>&1
BUILD_EXIT=$?
else
BUILD_EXIT=255
fi
echo "── interp (sx run) exit=$RUN_EXIT ───────────────────────"
cat "$RUN_LOG"
echo ""
echo "── codegen (sx build + spawn) exit=$BUILD_EXIT ─────────"
cat "$BUILD_LOG"
echo ""
if [[ "$RUN_EXIT" -ne "$BUILD_EXIT" ]]; then
echo "DIVERGENCE: exit codes differ (run=$RUN_EXIT build=$BUILD_EXIT)"
exit 1
fi
if diff -q "$RUN_LOG" "$BUILD_LOG" > /dev/null; then
echo "PARITY: interp and codegen agree (exit $RUN_EXIT)"
exit 0
else
echo "DIVERGENCE: stdout differs"
diff "$RUN_LOG" "$BUILD_LOG" || true
exit 1
fi

85
tools/verify-step.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/bin/bash
# tools/verify-step.sh
#
# Single-command verification gate run after every plan step.
# Per the mem.sx implementation plan, ~/projects/game must remain
# buildable + runnable on all 3 platforms (macOS host, iOS sim,
# Android device) at every step boundary.
#
# Exits 0 if all gates pass; non-zero on any failure.
# Screenshots saved to /tmp/sx-game-{macos,iossim,android}.png.
set -e
ROOT="/Users/agra/projects/sx"
GAME="/Users/agra/projects/game"
SX="$ROOT/zig-out/bin/sx"
cd "$ROOT"
echo "── 1/5 zig build ─────────────────────────────────────"
zig build
echo "── 2/5 zig build test ────────────────────────────────"
zig build test
echo "── 3/5 example regression suite ──────────────────────"
bash tests/run_examples.sh
echo "── 4/5 chess: cross-build for all 3 platforms ────────"
# Builds must be serial — sx writes to .sx-tmp/ which would race in parallel.
cd "$GAME"
"$SX" build main.sx > /tmp/sx-game-macos-build.log 2>&1 \
|| { echo "macOS build failed:"; cat /tmp/sx-game-macos-build.log; exit 1; }
echo " macOS OK"
"$SX" build --target ios-sim main.sx > /tmp/sx-game-iossim-build.log 2>&1 \
|| { echo "iOS sim build failed:"; cat /tmp/sx-game-iossim-build.log; exit 1; }
echo " iOS sim OK"
"$SX" build --target android main.sx > /tmp/sx-game-android-build.log 2>&1 \
|| { echo "Android build failed:"; cat /tmp/sx-game-android-build.log; exit 1; }
echo " Android OK"
echo "── 5/5 chess: launch + screenshot on each platform ───"
# macOS — direct binary launch
./sx-out/macos/SxChess > /tmp/sx-game-macos-run.log 2>&1 &
PID=$!
sleep 5
if ps -p $PID > /dev/null; then
screencapture -x /tmp/sx-game-macos.png
kill $PID 2>/dev/null
wait $PID 2>/dev/null
echo " macOS screenshot saved: /tmp/sx-game-macos.png"
else
echo " macOS process exited early; log:"
cat /tmp/sx-game-macos-run.log
exit 1
fi
# iOS sim — requires booted simulator
if xcrun simctl list devices booted 2>/dev/null | grep -q "Booted"; then
xcrun simctl install booted "$GAME/sx-out/ios/SxChess.app" > /dev/null 2>&1
xcrun simctl launch booted co.swipelab.sxchess > /dev/null 2>&1
sleep 5
xcrun simctl io booted screenshot /tmp/sx-game-iossim.png > /dev/null 2>&1
echo " iOS sim screenshot saved: /tmp/sx-game-iossim.png"
else
echo " iOS sim SKIPPED (no booted simulator)"
fi
# Android — requires connected device. Needs 6s+ for the side panel to render.
if adb devices 2>/dev/null | grep -q "device$"; then
adb install -r "$GAME/sx-out/android/sxchess.apk" > /dev/null 2>&1
adb shell am force-stop co.swipelab.sxchess > /dev/null 2>&1
adb shell am start -n co.swipelab.sxchess/.SxApp > /dev/null 2>&1
sleep 6
adb exec-out screencap -p > /tmp/sx-game-android.png 2>/dev/null
echo " Android screenshot saved: /tmp/sx-game-android.png"
else
echo " Android SKIPPED (no connected device)"
fi
cd "$ROOT"
echo ""
echo "═══ all gates pass ═════════════════════════════════════"
echo "screenshots: /tmp/sx-game-{macos,iossim,android}.png"