iOS lock step keyboard + metal
This commit is contained in:
32
examples/88-enum-through-protocol-dispatch.sx
Normal file
32
examples/88-enum-through-protocol-dispatch.sx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Sub-32-bit enum variants ride through a protocol-typed receiver's
|
||||||
|
// method call without being collapsed to tag=0.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Fmt :: enum { a; b; }
|
||||||
|
|
||||||
|
Proto :: protocol {
|
||||||
|
take_fmt :: (f: Fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
Impl :: struct {}
|
||||||
|
impl Proto for Impl {
|
||||||
|
take_fmt :: (self: *Impl, f: Fmt) {
|
||||||
|
n : s64 = xx f;
|
||||||
|
print("proto f = {}\n", n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
take :: (f: Fmt) -> s64 {
|
||||||
|
n : s64 = xx f;
|
||||||
|
n;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("direct a={} b={}\n", take(.a), take(.b));
|
||||||
|
|
||||||
|
p : Proto = xx @Impl.{};
|
||||||
|
p.take_fmt(.b);
|
||||||
|
p.take_fmt(.a);
|
||||||
|
0;
|
||||||
|
}
|
||||||
20
examples/89-enum-arg-through-closure-field.sx
Normal file
20
examples/89-enum-arg-through-closure-field.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// A closure stored in a struct field receives sub-32-bit enum args
|
||||||
|
// with the right tag, same as direct or protocol-dispatched calls.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Fmt :: enum { a; b; }
|
||||||
|
|
||||||
|
Ctx :: struct {
|
||||||
|
on: Closure(Fmt) -> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
c : Ctx = .{ on = (f: Fmt) => {
|
||||||
|
n : s64 = xx f;
|
||||||
|
print("cl f = {}\n", n);
|
||||||
|
}};
|
||||||
|
c.on(.b);
|
||||||
|
c.on(.a);
|
||||||
|
0;
|
||||||
|
}
|
||||||
30
examples/90-protocol-real-pointer-return.sx
Normal file
30
examples/90-protocol-real-pointer-return.sx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// A protocol method declared with a real pointer return (`-> *u8`,
|
||||||
|
// NOT `-> Self`) returns the raw pointer to the caller without the
|
||||||
|
// dispatch path auto-dereferencing it. Without this, a method whose
|
||||||
|
// pointee is a single byte gets `sizeof(target)` bytes loaded past
|
||||||
|
// it and segfaults.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Proto :: protocol {
|
||||||
|
get :: () -> *u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
Impl :: struct {
|
||||||
|
val: u8 = 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Proto for Impl {
|
||||||
|
get :: (self: *Impl) -> *u8 {
|
||||||
|
@self.val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
imp : Impl = .{};
|
||||||
|
p : Proto = xx @imp;
|
||||||
|
raw : *u8 = p.get();
|
||||||
|
addr_word : u64 = xx raw;
|
||||||
|
print("got pointer: {}\n", addr_word != 0);
|
||||||
|
0;
|
||||||
|
}
|
||||||
20
examples/91-protocol-typeparam-parse.sx
Normal file
20
examples/91-protocol-typeparam-parse.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Phase 1 (xx-via-Into mechanism): proves the new syntax parses + lowers
|
||||||
|
// without error. The parameterised protocol Into(Target: Type) and the
|
||||||
|
// matching `impl Into(Block) for Closure() -> void` declarations are
|
||||||
|
// registered but unused. Resolution (Phase 3) is what makes the impl
|
||||||
|
// reachable from `xx`.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
MyTag :: struct { value: s64 = 0; }
|
||||||
|
|
||||||
|
impl Into(MyTag) for s64 {
|
||||||
|
convert :: (self: s64) -> MyTag {
|
||||||
|
.{ value = self };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("ok\n");
|
||||||
|
0;
|
||||||
|
}
|
||||||
20
examples/92-xx-userspace.sx
Normal file
20
examples/92-xx-userspace.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Phase 3 (xx-via-Into): a user-defined `impl Into(Target) for Source`
|
||||||
|
// reaches the xx operator through compile-time dispatch. The compiler
|
||||||
|
// monomorphises `convert` for the (Source, Target) pair and emits a
|
||||||
|
// direct call — no vtable, identical to a hand-written call.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
MyString :: struct { tag: s64 = 0; }
|
||||||
|
|
||||||
|
impl Into(MyString) for s64 {
|
||||||
|
convert :: (self: s64) -> MyString {
|
||||||
|
.{ tag = self };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
x : MyString = xx 42;
|
||||||
|
print("tag = {}\n", x.tag);
|
||||||
|
0;
|
||||||
|
}
|
||||||
12
examples/93-into-impl-helper.sx
Normal file
12
examples/93-into-impl-helper.sx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Helper for 93-into-import-scope.sx: declares Wrap + an impl Into for it.
|
||||||
|
// No paired tests/expected file — not executed standalone by the runner.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Wrap :: struct { v: s64 = 0; }
|
||||||
|
|
||||||
|
impl Into(Wrap) for s64 {
|
||||||
|
convert :: (self: s64) -> Wrap {
|
||||||
|
.{ v = self * 10 };
|
||||||
|
}
|
||||||
|
}
|
||||||
13
examples/93-into-import-scope.sx
Normal file
13
examples/93-into-import-scope.sx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Phase 4 (xx-via-Into mechanism): an `impl Into(...) for ...` lives in
|
||||||
|
// a separate file and reaches the xx site through a direct `#import`.
|
||||||
|
// The visibility filter accepts the impl because the user file
|
||||||
|
// transitively imports the impl's defining module.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./93-into-impl-helper.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
w : Wrap = xx 3;
|
||||||
|
print("w.v = {}\n", w.v);
|
||||||
|
0;
|
||||||
|
}
|
||||||
14
examples/94-foreign-global.sx
Normal file
14
examples/94-foreign-global.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Extern data globals via `<name> : <type> #foreign;`. Lets sx code
|
||||||
|
// reference libSystem / framework symbols (NSConcreteStackBlock,
|
||||||
|
// __stdinp, etc.) for FFI bridges. Mirrors the long-standing
|
||||||
|
// `<fn> :: (...) -> ... #foreign;` form on the function side.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
__stdinp : *void #foreign;
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
addr_bits : u64 = xx @__stdinp;
|
||||||
|
print("stdin extern global non-null: {}\n", addr_bits != 0);
|
||||||
|
0;
|
||||||
|
}
|
||||||
17
examples/95-objc-block-noop.sx
Normal file
17
examples/95-objc-block-noop.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// `xx <closure> : Block` builds an Apple-ABI block whose invoke
|
||||||
|
// trampoline delegates to the sx closure. Verifies end-to-end:
|
||||||
|
// stdlib Block layout, _NSConcreteStackBlock extern, per-signature
|
||||||
|
// invoke trampoline, Into(Block) for Closure() -> void. Runs on
|
||||||
|
// macOS — invokes the block's invoke fn directly via a typed fn
|
||||||
|
// pointer instead of going through the Obj-C runtime.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/std/objc_block.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
cl := () => { print("noop block ran\n"); };
|
||||||
|
b : Block = xx cl;
|
||||||
|
invoke_fn : (*Block) -> void = xx b.invoke;
|
||||||
|
invoke_fn(@b);
|
||||||
|
0;
|
||||||
|
}
|
||||||
17
examples/96-objc-block-capture.sx
Normal file
17
examples/96-objc-block-capture.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// A capturing closure rides through `xx ... : Block` and the
|
||||||
|
// captured state survives across the call. The block's sx_env field
|
||||||
|
// holds the closure's env pointer; the invoke trampoline restores it
|
||||||
|
// before delegating.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/std/objc_block.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
x : s64 = 42;
|
||||||
|
y : s64 = 100;
|
||||||
|
cl := () => { print("x + y = {}\n", x + y); };
|
||||||
|
b : Block = xx cl;
|
||||||
|
invoke_fn : (*Block) -> void = xx b.invoke;
|
||||||
|
invoke_fn(@b);
|
||||||
|
0;
|
||||||
|
}
|
||||||
19
examples/97-objc-block-inline.sx
Normal file
19
examples/97-objc-block-inline.sx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// `xx <closure>` passed as a `*Block` fn argument auto-allocates the
|
||||||
|
// Block instance and passes its address — no named temp required.
|
||||||
|
// Matches the ergonomics of ObjC's `^{...}` literal at the call site.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/std/objc_block.sx";
|
||||||
|
|
||||||
|
invoke_once :: (b: *Block) {
|
||||||
|
invoke_fn : (*Block) -> void = xx b.invoke;
|
||||||
|
invoke_fn(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
x : s64 = 7;
|
||||||
|
invoke_once(xx () => {
|
||||||
|
print("inline block, x = {}\n", x);
|
||||||
|
});
|
||||||
|
0;
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// issue-0026: Chess game on iOS-sim with `plat.gpu_mode = .metal` crashes
|
|
||||||
// inside `[MTLTexture replaceRegion:mipmapLevel:withBytes:bytesPerRow:]`
|
|
||||||
// when uploading the 1024×1024 R8 font atlas. The 1×1 RGBA8 white tex
|
|
||||||
// through the SAME code path (metal_update_texture_region_ios in
|
|
||||||
// library/modules/gpu/metal.sx) works.
|
|
||||||
//
|
|
||||||
// Blocked on issue-0024 (NSLog inside if/else not firing — or unified-log
|
|
||||||
// buffer loss on crash; investigation pending) — without a trustworthy
|
|
||||||
// tracer we can't reliably bisect which arg arrives wrong. Most likely
|
|
||||||
// cause: this is downstream of issue-0025's ABI gaps (MTLRegion is 48
|
|
||||||
// bytes and goes through `xx objc_msgSend` cast, which is the
|
|
||||||
// call_indirect path that issue-0025 part B covers).
|
|
||||||
//
|
|
||||||
// ── Reproduction recipe ───────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// cd /Users/agra/projects/game
|
|
||||||
// /Users/agra/projects/sx/zig-out/bin/sx build --target ios-sim main.sx \
|
|
||||||
// --bundle sx-out/ios/SxChess.app --bundle-id co.swipelab.sxchess \
|
|
||||||
// -F ~/Library/Frameworks
|
|
||||||
// cp -R assets sx-out/ios/SxChess.app/
|
|
||||||
// codesign --force --sign - --timestamp=none sx-out/ios/SxChess.app
|
|
||||||
// xcrun simctl install booted sx-out/ios/SxChess.app
|
|
||||||
// xcrun simctl launch --terminate-running-process booted co.swipelab.sxchess
|
|
||||||
// sleep 4 && xcrun simctl io booted screenshot /tmp/sx-chess.png
|
|
||||||
//
|
|
||||||
// Expected (after fix): chess board renders via Metal.
|
|
||||||
// Observed: app launches, returns immediately to home screen, no screen
|
|
||||||
// touched. The simpler examples/63-metal-clear.sx demo still renders the
|
|
||||||
// colored triangle on the same sim, so the Metal pipeline itself works
|
|
||||||
// for small uploads.
|
|
||||||
//
|
|
||||||
// ── Candidate root causes (in priority order) ─────────────────────────────
|
|
||||||
//
|
|
||||||
// 1. issue-0025 fallout (most likely): MTLRegion (48 B by value) passed
|
|
||||||
// via the *MTLRegion workaround. The call_indirect path (issue-0025
|
|
||||||
// part B) doesn't ABI-coerce, so the pointer-shaped declaration may
|
|
||||||
// not actually pass the address in the right register slot for that
|
|
||||||
// call site shape (6 args, including the indirect aggregate).
|
|
||||||
//
|
|
||||||
// 2. iOS-sim Metal-driver limitation: `setStorageMode:.shared` may not be
|
|
||||||
// honored for r8 textures of this size; default may be `.private`
|
|
||||||
// which precludes CPU-side replaceRegion. Workaround would be to
|
|
||||||
// upload via `MTLBuffer` + `MTLBlitCommandEncoder` (newBufferWithBytes
|
|
||||||
// + copyFromBuffer:sourceOffset:sourceBytesPerRow:...:toTexture:...).
|
|
||||||
//
|
|
||||||
// 3. sx-side `xx` cast bug: bytes_per_row : u64 = xx (u32_expr) may
|
|
||||||
// truncate or sign-extend incorrectly. Less likely (the math comes
|
|
||||||
// out to 1024, which fits in any width).
|
|
||||||
//
|
|
||||||
// ── How to resolve ────────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// After issues 0024 + 0025 are landed:
|
|
||||||
// 1. Re-add the trace NSLog markers ("[metal] U1..U5" in
|
|
||||||
// metal_update_texture_region_ios) — now they should actually print.
|
|
||||||
// 2. Re-build + relaunch chess on iOS-sim.
|
|
||||||
// 3. If U5 fires after U4 (no crash inside msg_replace), the bug was
|
|
||||||
// ABI-related; declare success and rename this file to
|
|
||||||
// examples/NN-metal-large-region-upload.sx (next free NN).
|
|
||||||
// 4. If U4 → crash persists, fall back to the MTLBuffer + blit
|
|
||||||
// encoder path in metal.sx's create_texture (when pixels != null,
|
|
||||||
// allocate a temporary MTLBuffer with newBufferWithBytes:length:options:
|
|
||||||
// then run a one-shot command buffer with a MTLBlitCommandEncoder
|
|
||||||
// copying the buffer into the texture). This is the Apple-recommended
|
|
||||||
// approach for large texture initial-uploads.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
|
||||||
|
|
||||||
main :: () -> s32 { 0; }
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// issue-0027: Feature — support Obj-C blocks (^{...}) so sx code can call
|
|
||||||
// APIs that take a block parameter. Required for step 4 of the Metal port
|
|
||||||
// (keyboard lockstep via `[UIView animateWithDuration:animations:^{...}]`),
|
|
||||||
// and broadly useful for any UIKit/AppKit API.
|
|
||||||
//
|
|
||||||
// ── Proposed surface ──────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Option A — comptime intrinsic that wraps a sx closure as a block:
|
|
||||||
//
|
|
||||||
// block := objc_block(@my_closure); // returns *void (an id<Block>)
|
|
||||||
// msg_block(view, sel, 0.3, block); // pass like any id arg
|
|
||||||
//
|
|
||||||
// Internals: emit a Block_literal struct constant with the right invoke
|
|
||||||
// fn pointer, isa, flags, descriptor pointer. Approximately what clang
|
|
||||||
// generates for ^{...}.
|
|
||||||
//
|
|
||||||
// Option B — surface-level syntax `^{ ... }` that lowers to Option A
|
|
||||||
// automatically. Cleaner for users; more parser work.
|
|
||||||
//
|
|
||||||
// Recommended: start with Option A (intrinsic). Migrate to Option B once
|
|
||||||
// the codegen path is proven.
|
|
||||||
//
|
|
||||||
// ── Implementation sketch ────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// 1. New `library/modules/std/objc_block.sx` defining the Block_literal
|
|
||||||
// struct that mirrors clang's layout (isa, flags, reserved, invoke fn
|
|
||||||
// pointer, descriptor pointer).
|
|
||||||
// 2. `objc_block(fn_or_closure) -> *void` intrinsic that builds the
|
|
||||||
// literal at the call site. Initial implementation can be a
|
|
||||||
// stack-allocated block (_NSConcreteStackBlock); upgrade to
|
|
||||||
// heap-promoted (_Block_copy) once block lifetime exceeds the call.
|
|
||||||
// 3. Link libSystem's symbols `_NSConcreteStackBlock` and
|
|
||||||
// `_NSConcreteGlobalBlock` (auto on iOS; may need `#library "System"`
|
|
||||||
// on macOS).
|
|
||||||
// 4. (Deferred) surface syntax `^{ ... }` — parser hook + lowering
|
|
||||||
// to the intrinsic. Must not clash with bitwise XOR `^`.
|
|
||||||
//
|
|
||||||
// ── References ────────────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// - Apple block ABI spec (clang's "Block Implementation Specification")
|
|
||||||
// - _NSConcreteStackBlock + _NSConcreteGlobalBlock from libSystem
|
|
||||||
//
|
|
||||||
// ── Real-world impact ─────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Without this, the keyboard inset cannot be animated in lockstep with the
|
|
||||||
// keyboard slide. See library/modules/platform/uikit.sx's
|
|
||||||
// uikit_keyboard_will_change_frame comments for the deferred lockstep work.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
|
||||||
main :: () -> s32 { 0; }
|
|
||||||
17
examples/issue-0032.sx
Normal file
17
examples/issue-0032.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Phase 2 verification: two impls for the same parameterised-protocol
|
||||||
|
// (Source, Target) pair declared in the same file MUST produce a clean
|
||||||
|
// "duplicate impl" diagnostic at registration time.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
MyA :: struct { v: s64 = 0; }
|
||||||
|
|
||||||
|
impl Into(MyA) for s64 {
|
||||||
|
convert :: (self: s64) -> MyA { .{ v = self }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into(MyA) for s64 {
|
||||||
|
convert :: (self: s64) -> MyA { .{ v = self * 2 }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 { 0; }
|
||||||
10
examples/issue-0033-impl.sx
Normal file
10
examples/issue-0033-impl.sx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Helper that defines the impl. issue-0033's user file does NOT
|
||||||
|
// directly import this — that's the whole point of the test.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./issue-0033-types.sx";
|
||||||
|
|
||||||
|
impl Into(Wrap) for s64 {
|
||||||
|
convert :: (self: s64) -> Wrap {
|
||||||
|
.{ v = self * 10 };
|
||||||
|
}
|
||||||
|
}
|
||||||
2
examples/issue-0033-types.sx
Normal file
2
examples/issue-0033-types.sx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Shared type for issue-0033 — Wrap struct.
|
||||||
|
Wrap :: struct { v: s64 = 0; }
|
||||||
10
examples/issue-0033-user.sx
Normal file
10
examples/issue-0033-user.sx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// User file uses xx but only imports the shared types — NOT the impl.
|
||||||
|
// The Phase 4 visibility filter should reject the impl from issue-0033-impl.sx.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./issue-0033-types.sx";
|
||||||
|
|
||||||
|
run_user :: () -> s32 {
|
||||||
|
w : Wrap = xx 7;
|
||||||
|
print("user: w.v = {}\n", w.v);
|
||||||
|
0;
|
||||||
|
}
|
||||||
16
examples/issue-0033.sx
Normal file
16
examples/issue-0033.sx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Phase 4 verification: an `impl Into(...) for ...` is registered into the
|
||||||
|
// global impl table when its module is imported anywhere in the program, but
|
||||||
|
// is only **visible** from files that themselves transitively import the impl's
|
||||||
|
// defining module. Here:
|
||||||
|
// - issue-0033-impl.sx declares an `impl Into(Wrap) for s64`.
|
||||||
|
// - issue-0033-user.sx tries to `xx 7 : Wrap` but only imports the shared
|
||||||
|
// types — not the impl module.
|
||||||
|
// - The xx at issue-0033-user.sx:7 must produce a clean "no visible xx
|
||||||
|
// conversion" diagnostic, not silently fall through to whatever was
|
||||||
|
// registered in another module.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./issue-0033-impl.sx";
|
||||||
|
#import "./issue-0033-user.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 { run_user(); }
|
||||||
9
examples/issue-0034-impl-a.sx
Normal file
9
examples/issue-0034-impl-a.sx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Helper A — one of two conflicting impls for the same (s64, Wrap) pair.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./issue-0034-types.sx";
|
||||||
|
|
||||||
|
impl Into(Wrap) for s64 {
|
||||||
|
convert :: (self: s64) -> Wrap {
|
||||||
|
.{ v = self * 10 };
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/issue-0034-impl-b.sx
Normal file
9
examples/issue-0034-impl-b.sx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Helper B — second conflicting impl for the same (s64, Wrap) pair.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./issue-0034-types.sx";
|
||||||
|
|
||||||
|
impl Into(Wrap) for s64 {
|
||||||
|
convert :: (self: s64) -> Wrap {
|
||||||
|
.{ v = self + 100 };
|
||||||
|
}
|
||||||
|
}
|
||||||
2
examples/issue-0034-types.sx
Normal file
2
examples/issue-0034-types.sx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Shared type for issue-0034.
|
||||||
|
Wrap :: struct { v: s64 = 0; }
|
||||||
14
examples/issue-0034.sx
Normal file
14
examples/issue-0034.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Phase 5 verification: two impls for the same (Source, Target) pair are
|
||||||
|
// both visible from the same xx site (because both their defining modules
|
||||||
|
// are transitively imported). The compiler must emit a clean
|
||||||
|
// "duplicate xx conversion" diagnostic naming both modules.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "./issue-0034-impl-a.sx";
|
||||||
|
#import "./issue-0034-impl-b.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
w : Wrap = xx 7;
|
||||||
|
print("w.v = {}\n", w.v);
|
||||||
|
0;
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ FrameContext :: struct {
|
|||||||
pixel_h: s32;
|
pixel_h: s32;
|
||||||
dpi_scale: f32;
|
dpi_scale: f32;
|
||||||
delta_time: f32;
|
delta_time: f32;
|
||||||
|
// The host clock time at which the next vsync will present the frame
|
||||||
|
// we're about to render. On iOS this is CADisplayLink.targetTimestamp;
|
||||||
|
// forward it to MetalGPU.end_frame() to schedule presentDrawable:atTime:
|
||||||
|
// so our drawable hits the same vsync as UIKit's compositor. Other
|
||||||
|
// platforms leave it 0 (Metal then falls back to immediate present).
|
||||||
|
target_present_time: f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardState :: struct {
|
KeyboardState :: struct {
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_c
|
|||||||
dlsym :: (handle: *void, name: [*]u8) -> *void #foreign;
|
dlsym :: (handle: *void, name: [*]u8) -> *void #foreign;
|
||||||
chdir :: (path: [*]u8) -> s32 #foreign;
|
chdir :: (path: [*]u8) -> s32 #foreign;
|
||||||
|
|
||||||
|
// QuartzCore's wall-clock helper used by CoreAnimation. Seconds since boot,
|
||||||
|
// monotonic. We use it as the timebase for keyboard-inset lockstep so the
|
||||||
|
// per-frame interpolation lines up with UIKit's own animation timestamp.
|
||||||
|
CACurrentMediaTime :: () -> f64 #foreign;
|
||||||
|
|
||||||
|
|
||||||
// kEAGLRenderingAPIOpenGLES3 = 3
|
// kEAGLRenderingAPIOpenGLES3 = 3
|
||||||
EAGL_API_GLES3 :: 3;
|
EAGL_API_GLES3 :: 3;
|
||||||
|
|
||||||
@@ -86,6 +92,9 @@ UIKitPlatform :: struct {
|
|||||||
dpi_scale: f32 = 1.0;
|
dpi_scale: f32 = 1.0;
|
||||||
|
|
||||||
delta_time: f32 = 0.016;
|
delta_time: f32 = 0.016;
|
||||||
|
// Latest CADisplayLink.targetTimestamp captured each tick — forwarded
|
||||||
|
// through FrameContext to MetalGPU.end_frame() for presentDrawable:atTime:.
|
||||||
|
last_target_ts: f64 = 0.0;
|
||||||
|
|
||||||
frame_closure: Closure() = ---;
|
frame_closure: Closure() = ---;
|
||||||
has_frame_closure: bool = false;
|
has_frame_closure: bool = false;
|
||||||
@@ -100,16 +109,21 @@ UIKitPlatform :: struct {
|
|||||||
keyboard_visible: bool = false;
|
keyboard_visible: bool = false;
|
||||||
keyboard_height: f32 = 0.0;
|
keyboard_height: f32 = 0.0;
|
||||||
|
|
||||||
// Keyboard height SNAPS to its target value when the observer fires.
|
// Keyboard inset lockstep: when willChangeFrame fires we read the
|
||||||
// It does NOT interpolate in lockstep with iOS's keyboard animation.
|
// animation duration from the notification's userInfo and interpolate
|
||||||
// Reason: with OpenGL ES + CAEAGLLayer, our renderbuffer is baked at
|
// `keyboard_height` over that window on each display-link tick. Each
|
||||||
// `presentRenderbuffer` time, while UIKit's keyboard view is composited
|
// animation has a fresh start time and target — if a second event
|
||||||
// by CoreAnimation at vsync. We can't make the compositor interpolate
|
// arrives mid-animation, the next interpolation starts from the
|
||||||
// the renderbuffer's contents in lockstep with the keyboard's frame.
|
// currently-interpolated value (not from the previous animation's
|
||||||
// True lockstep requires a Metal renderer (CAMetalLayer +
|
// origin). The easing is `smoothstep` (cubic Hermite) which closely
|
||||||
// `present(at: targetTimestamp)` keeps the pipeline at 1 frame) plus
|
// approximates UIKit's keyboard curve to within a frame at the
|
||||||
// curve-accurate prediction. Tracked as the Metal port in
|
// standard 0.25s slide duration.
|
||||||
// current/CHECKPOINT.md.
|
kb_anim_from: f32 = 0.0;
|
||||||
|
kb_anim_to: f32 = 0.0;
|
||||||
|
kb_anim_start: f64 = 0.0;
|
||||||
|
kb_anim_dur: f64 = 0.0;
|
||||||
|
kb_anim_curve: u64 = 0;
|
||||||
|
kb_animating: bool = false;
|
||||||
|
|
||||||
saved_title: [*]u8 = null;
|
saved_title: [*]u8 = null;
|
||||||
}
|
}
|
||||||
@@ -170,6 +184,7 @@ impl Platform for UIKitPlatform {
|
|||||||
pixel_h = self.pixel_h,
|
pixel_h = self.pixel_h,
|
||||||
dpi_scale = self.dpi_scale,
|
dpi_scale = self.dpi_scale,
|
||||||
delta_time = self.delta_time,
|
delta_time = self.delta_time,
|
||||||
|
target_present_time = self.last_target_ts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,11 +388,17 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
|
|||||||
if end_value == null { return; }
|
if end_value == null { return; }
|
||||||
end_rect := msg_rect(end_value, sel_cg_rect_value);
|
end_rect := msg_rect(end_value, sel_cg_rect_value);
|
||||||
|
|
||||||
// UIKeyboardAnimationDurationUserInfoKey is also in userInfo; reading it
|
dur_value := msg_oo(user_info, sel_obj_for_key,
|
||||||
// and running our inset update inside a `[UIView animateWithDuration:...]`
|
ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr));
|
||||||
// block would put us in the same CoreAnimation transaction as the keyboard
|
anim_dur : f64 = 0.0;
|
||||||
// (zero-lag sync). Blocks aren't yet expressible from sx, so we update the
|
if dur_value != null { anim_dur = msg_d(dur_value, sel_double_value); }
|
||||||
// inset synchronously — content snaps while the keyboard slides.
|
|
||||||
|
sel_unsigned_long_value := sel_registerName("unsignedLongValue".ptr);
|
||||||
|
msg_ul : (*void, *void) -> u64 = xx objc_msgSend;
|
||||||
|
curve_value := msg_oo(user_info, sel_obj_for_key,
|
||||||
|
ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr));
|
||||||
|
curve_int : u64 = 0;
|
||||||
|
if curve_value != null { curve_int = msg_ul(curve_value, sel_unsigned_long_value); }
|
||||||
|
|
||||||
// Screen height in points. The window lives on the connected scene's screen.
|
// Screen height in points. The window lives on the connected scene's screen.
|
||||||
if plat.window == null { return; }
|
if plat.window == null { return; }
|
||||||
@@ -392,11 +413,34 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
|
|||||||
h := sh - kb_top;
|
h := sh - kb_top;
|
||||||
if h < 0.0 { h = 0.0; }
|
if h < 0.0 { h = 0.0; }
|
||||||
if h > sh { h = sh; }
|
if h > sh { h = sh; }
|
||||||
|
target_h : f32 = xx h;
|
||||||
|
|
||||||
// SNAP to target. See comment on UIKitPlatform.keyboard_height for why
|
|
||||||
// lockstep interpolation is deferred until the Metal renderer.
|
|
||||||
plat.keyboard_height = xx h;
|
|
||||||
plat.keyboard_visible = h > 0.5;
|
plat.keyboard_visible = h > 0.5;
|
||||||
|
|
||||||
|
if anim_dur <= 0.0 {
|
||||||
|
// No animation window — snap.
|
||||||
|
plat.keyboard_height = target_h;
|
||||||
|
plat.kb_animating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the animation params for the sx-side per-tick interpolation
|
||||||
|
// that drives `keyboard_height` (consumers like the chess UI's safe-area
|
||||||
|
// calc read it each frame). The interpolation uses cubic ease-out as a
|
||||||
|
// close approximation of UIKit's keyboard curve. For perfect lockstep
|
||||||
|
// on a UIView consumer in user code, drive a property via
|
||||||
|
// `[UIView animateWithDuration:plat.kb_anim_dur delay:0
|
||||||
|
// options:(plat.kb_anim_curve << 16) | 4
|
||||||
|
// animations:^{ ... }
|
||||||
|
// completion:nil]`
|
||||||
|
// — UIKit's internal options-to-CAMediaTimingFunction table handles
|
||||||
|
// even the private keyboard curve 7 correctly when packed this way.
|
||||||
|
plat.kb_anim_from = plat.keyboard_height;
|
||||||
|
plat.kb_anim_to = target_h;
|
||||||
|
plat.kb_anim_start = CACurrentMediaTime();
|
||||||
|
plat.kb_anim_dur = anim_dur;
|
||||||
|
plat.kb_anim_curve = curve_int;
|
||||||
|
plat.kb_animating = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
uikit_create_gl_context :: (plat: *UIKitPlatform) {
|
uikit_create_gl_context :: (plat: *UIKitPlatform) {
|
||||||
@@ -603,6 +647,7 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
|
|||||||
plat.text_field = msg_o(tf_raw, sel_init);
|
plat.text_field = msg_o(tf_raw, sel_init);
|
||||||
msg_oo(plat.gl_view, sel_add_subview, plat.text_field);
|
msg_oo(plat.gl_view, sel_add_subview, plat.text_field);
|
||||||
|
|
||||||
|
|
||||||
// (Keyboard observer is registered in didFinishLaunching via
|
// (Keyboard observer is registered in didFinishLaunching via
|
||||||
// uikit_subscribe_keyboard_notifications — it's app-level, not scene-
|
// uikit_subscribe_keyboard_notifications — it's app-level, not scene-
|
||||||
// level, so it doesn't belong here.)
|
// level, so it doesn't belong here.)
|
||||||
@@ -678,13 +723,54 @@ uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
|
|||||||
uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
|
uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
|
||||||
if g_uikit_plat == null { return; }
|
if g_uikit_plat == null { return; }
|
||||||
plat := g_uikit_plat;
|
plat := g_uikit_plat;
|
||||||
|
|
||||||
|
// Keyboard-inset lockstep — sx-side cubic ease-out approximation of
|
||||||
|
// UIKit's private keyboard curve. Sample targetTimestamp so we
|
||||||
|
// interpolate at the time this frame will be visible. Lags by ~1
|
||||||
|
// frame behind UIKit because UIKit's keyboard is rendered in a
|
||||||
|
// separate process (UIRemoteKeyboardWindow) and we can't perfectly
|
||||||
|
// sync to it from outside that scene. Refinements tried:
|
||||||
|
// CATransaction.flush, CABasicAnimation, presentationLayer reading,
|
||||||
|
// and keyboardLayoutGuide — none eliminated the lag without
|
||||||
|
// cascade-breaking the GL view's frame.
|
||||||
|
if plat.kb_animating {
|
||||||
|
sel_target_ts := sel_registerName("targetTimestamp".ptr);
|
||||||
|
msg_d2 : (*void, *void) -> f64 = xx objc_msgSend;
|
||||||
|
target_ts := msg_d2(link, sel_target_ts);
|
||||||
|
elapsed := target_ts - plat.kb_anim_start;
|
||||||
|
// Negative elapsed can happen if the just-fired willChangeFrame
|
||||||
|
// set kb_anim_start to a wall time AFTER the tick already
|
||||||
|
// captured its targetTimestamp this frame. Without the clamp,
|
||||||
|
// t < 0 makes the cubic ease-out *overshoot* in the opposite
|
||||||
|
// direction (visible as the indicator briefly jumping past the
|
||||||
|
// keyboard on close, then animating back).
|
||||||
|
if elapsed < 0.0 { elapsed = 0.0; }
|
||||||
|
if elapsed >= plat.kb_anim_dur or plat.kb_anim_dur <= 0.0 {
|
||||||
|
plat.keyboard_height = plat.kb_anim_to;
|
||||||
|
plat.kb_animating = false;
|
||||||
|
} else {
|
||||||
|
t : f32 = xx (elapsed / plat.kb_anim_dur);
|
||||||
|
inv := 1.0 - t;
|
||||||
|
eased := 1.0 - inv * inv * inv;
|
||||||
|
plat.keyboard_height = plat.kb_anim_from + (plat.kb_anim_to - plat.kb_anim_from) * eased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicator's position is driven by UIView.animateWithDuration kicked
|
||||||
|
// off from willChangeFrame — it animates in lockstep with UIKit's
|
||||||
|
// keyboard using the same curve+duration. No per-tick setFrame here.
|
||||||
|
|
||||||
if !plat.has_frame_closure { return; }
|
if !plat.has_frame_closure { return; }
|
||||||
if !plat.gl_initialized { return; }
|
if !plat.gl_initialized { return; }
|
||||||
|
|
||||||
sel_dur := sel_registerName("duration".ptr);
|
sel_dur := sel_registerName("duration".ptr);
|
||||||
|
sel_tts := sel_registerName("targetTimestamp".ptr);
|
||||||
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
|
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
|
||||||
dur_d : f64 = msg_d(link, sel_dur);
|
dur_d : f64 = msg_d(link, sel_dur);
|
||||||
plat.delta_time = xx dur_d;
|
plat.delta_time = xx dur_d;
|
||||||
|
// Stash the targetTimestamp so begin_frame can hand it down to the
|
||||||
|
// game in FrameContext for Metal presentDrawable:atTime:.
|
||||||
|
plat.last_target_ts = msg_d(link, sel_tts);
|
||||||
|
|
||||||
fn := plat.frame_closure;
|
fn := plat.frame_closure;
|
||||||
fn();
|
fn();
|
||||||
|
|||||||
@@ -329,6 +329,15 @@ print :: ($fmt: string, args: ..Any) {
|
|||||||
#insert "out(result);";
|
#insert "out(result);";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User-space `xx` extension. `xx val : T` where the built-in conversion
|
||||||
|
// ladder makes no progress falls through to an `impl Into(T) for Source`
|
||||||
|
// lookup; the compiler monomorphises `convert` for the (Source, T) pair
|
||||||
|
// and emits a direct call. Compile-time only — no vtable, no runtime
|
||||||
|
// dispatch.
|
||||||
|
Into :: protocol(Target: Type) {
|
||||||
|
convert :: () -> Target;
|
||||||
|
}
|
||||||
|
|
||||||
List :: struct ($T: Type) {
|
List :: struct ($T: Type) {
|
||||||
items: [*]T = null;
|
items: [*]T = null;
|
||||||
len: s64 = 0;
|
len: s64 = 0;
|
||||||
|
|||||||
99
library/modules/std/objc_block.sx
Normal file
99
library/modules/std/objc_block.sx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Obj-C blocks bridged to sx closures.
|
||||||
|
//
|
||||||
|
// Apple's block ABI (clang's "Block Implementation Specification"): a block
|
||||||
|
// pointer is a struct whose first five fields are { isa, flags, reserved,
|
||||||
|
// invoke, descriptor } followed by per-block captured state. When an API
|
||||||
|
// like `[UIView animateWithDuration:animations:]` receives a block, the
|
||||||
|
// runtime reads `invoke` and calls it with the block pointer as the first
|
||||||
|
// argument. UIKit / Foundation callers always `_Block_copy` synchronously
|
||||||
|
// before returning, so a stack-allocated block is safe to pass directly.
|
||||||
|
//
|
||||||
|
// We layer the sx closure onto Apple's layout by appending two pointer
|
||||||
|
// fields to the standard 32-byte header: `sx_env` (the closure's captured
|
||||||
|
// environment pointer) and `sx_fn` (the closure trampoline). The per-
|
||||||
|
// signature `__block_invoke_*` C-ABI fn knows the offsets and calls
|
||||||
|
// through to `sx_fn(sx_env, args...)`.
|
||||||
|
//
|
||||||
|
// ── Lifetime contract ───────────────────────────────────────────────────
|
||||||
|
// `xx <closure> : *Block` returns a pointer into the surrounding sx
|
||||||
|
// function's stack frame. Same rule as `&local_var`: pass it directly to
|
||||||
|
// a callee that consumes it immediately or `_Block_copy`s internally
|
||||||
|
// (UIKit/Foundation always do). Don't store the pointer to use after the
|
||||||
|
// caller returns. If you need that, ship a `Block_copy`-backed sibling
|
||||||
|
// API and use it instead.
|
||||||
|
|
||||||
|
// Standard 32-byte block header plus two trailing slots for the sx closure
|
||||||
|
// it wraps. Total = 48 bytes.
|
||||||
|
Block :: struct {
|
||||||
|
isa: *void;
|
||||||
|
flags: s32;
|
||||||
|
reserved: s32;
|
||||||
|
invoke: *void;
|
||||||
|
descriptor: *void;
|
||||||
|
sx_env: *void;
|
||||||
|
sx_fn: *void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-block-shape metadata. The runtime reads `size` when copying the
|
||||||
|
// block to the heap, so it must equal the actual instance size.
|
||||||
|
BlockDescriptor :: struct {
|
||||||
|
reserved: u64;
|
||||||
|
size: u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// libSystem isa pointer for stack-allocated blocks. Resolved at link time
|
||||||
|
// (auto-linked on every Apple target via libSystem).
|
||||||
|
_NSConcreteStackBlock : *void #foreign;
|
||||||
|
|
||||||
|
// Shared descriptor for the 48-byte sx-block layout. All Into impls below
|
||||||
|
// point their `descriptor` field at this.
|
||||||
|
__sx_block_descriptor : BlockDescriptor = .{
|
||||||
|
reserved = 0,
|
||||||
|
size = 48,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-signature invoke trampolines. Each one reads sx_env + sx_fn from
|
||||||
|
// its block_self argument and tail-calls the closure through a typed
|
||||||
|
// fn-ptr cast. One per Apple block signature we support.
|
||||||
|
//
|
||||||
|
// Signature: `void (^)(void)` — no args, no return. The single most
|
||||||
|
// common Apple block shape (UIView animation bodies, dispatch_async, etc).
|
||||||
|
__block_invoke_void :: (block_self: *Block) callconv(.c) {
|
||||||
|
typed_fn : (*void) -> void = xx block_self.sx_fn;
|
||||||
|
typed_fn(block_self.sx_env);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into(Block) for Closure() -> void {
|
||||||
|
convert :: (self: Closure() -> void) -> Block {
|
||||||
|
.{
|
||||||
|
isa = @_NSConcreteStackBlock,
|
||||||
|
flags = 0,
|
||||||
|
reserved = 0,
|
||||||
|
invoke = xx @__block_invoke_void,
|
||||||
|
descriptor = xx @__sx_block_descriptor,
|
||||||
|
sx_env = self.env,
|
||||||
|
sx_fn = self.fn_ptr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature: `void (^)(BOOL)` — UIView animation completion handlers and
|
||||||
|
// similar one-arg-bool callbacks.
|
||||||
|
__block_invoke_bool :: (block_self: *Block, arg0: bool) callconv(.c) {
|
||||||
|
typed_fn : (*void, bool) -> void = xx block_self.sx_fn;
|
||||||
|
typed_fn(block_self.sx_env, arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into(Block) for Closure(bool) -> void {
|
||||||
|
convert :: (self: Closure(bool) -> void) -> Block {
|
||||||
|
.{
|
||||||
|
isa = @_NSConcreteStackBlock,
|
||||||
|
flags = 0,
|
||||||
|
reserved = 0,
|
||||||
|
invoke = xx @__block_invoke_bool,
|
||||||
|
descriptor = xx @__sx_block_descriptor,
|
||||||
|
sx_env = self.env,
|
||||||
|
sx_fn = self.fn_ptr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#import "modules/gpu/types.sx";
|
#import "modules/gpu/types.sx";
|
||||||
#import "modules/gpu/api.sx";
|
#import "modules/gpu/api.sx";
|
||||||
#import "modules/stb_truetype.sx";
|
#import "modules/stb_truetype.sx";
|
||||||
|
#import "modules/stb.sx";
|
||||||
#import "modules/ui/types.sx";
|
#import "modules/ui/types.sx";
|
||||||
|
|
||||||
// Cached glyph data with UV coordinates into the atlas texture
|
// Cached glyph data with UV coordinates into the atlas texture
|
||||||
@@ -426,17 +427,26 @@ GlyphCache :: struct {
|
|||||||
context.allocator.dealloc(old_vals);
|
context.allocator.dealloc(old_vals);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload dirty atlas to GPU
|
// Upload dirty atlas to GPU. On the Metal path, defer the upload to
|
||||||
|
// end-of-frame (`upload_atlas_to_gpu`) — calling `replaceRegion:` against
|
||||||
|
// the same R8 MTLTexture multiple times within one frame garbles the
|
||||||
|
// contents on iOS-sim Metal. The dirty flag carries over so the final
|
||||||
|
// end-of-frame upload picks up every rasterization that happened during
|
||||||
|
// the frame's render pass.
|
||||||
flush :: (self: *GlyphCache) {
|
flush :: (self: *GlyphCache) {
|
||||||
if self.dirty == false { return; }
|
if self.dirty == false { return; }
|
||||||
if self.has_gpu {
|
if self.has_gpu { return; }
|
||||||
self.gpu.update_texture_region(self.texture_id, 0, 0,
|
glBindTexture(GL_TEXTURE_2D, self.texture_id);
|
||||||
self.atlas_width, self.atlas_height, xx self.bitmap);
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
} else {
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self.atlas_width, self.atlas_height, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
|
||||||
glBindTexture(GL_TEXTURE_2D, self.texture_id);
|
self.dirty = false;
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
}
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self.atlas_width, self.atlas_height, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
|
|
||||||
}
|
upload_atlas_to_gpu :: (self: *GlyphCache) {
|
||||||
|
if self.has_gpu == false { return; }
|
||||||
|
if self.dirty == false { return; }
|
||||||
|
self.gpu.update_texture_region(self.texture_id, 0, 0,
|
||||||
|
self.atlas_width, self.atlas_height, xx self.bitmap);
|
||||||
self.dirty = false;
|
self.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ UIPipeline :: struct {
|
|||||||
|
|
||||||
self.renderer.begin(self.screen_width, self.screen_height, self.font.texture_id);
|
self.renderer.begin(self.screen_width, self.screen_height, self.font.texture_id);
|
||||||
self.renderer.process(@self.render_tree);
|
self.renderer.process(@self.render_tree);
|
||||||
|
// Push any glyphs rasterized during process() to the GPU atlas BEFORE
|
||||||
|
// the final draw is recorded. On Metal we deferred per-render_text
|
||||||
|
// uploads so this is the single point where the atlas reaches the
|
||||||
|
// GPU. On the GL path it's a no-op (uploads already happened inline).
|
||||||
|
self.font.upload_atlas_to_gpu();
|
||||||
self.renderer.flush();
|
self.renderer.flush();
|
||||||
|
|
||||||
if !self.has_gpu {
|
if !self.has_gpu {
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ UIRenderer :: struct {
|
|||||||
u1 := cached.uv_x + cached.uv_w;
|
u1 := cached.uv_x + cached.uv_w;
|
||||||
v1 := cached.uv_y + cached.uv_h;
|
v1 := cached.uv_y + cached.uv_h;
|
||||||
|
|
||||||
|
|
||||||
if self.vertex_count + 6 > MAX_UI_VERTICES {
|
if self.vertex_count + 6 > MAX_UI_VERTICES {
|
||||||
self.flush();
|
self.flush();
|
||||||
}
|
}
|
||||||
@@ -619,12 +620,7 @@ fragment float4 fmain(VOut in [[stage_in]],
|
|||||||
// Image mode (mode == -2.0): sample texture
|
// Image mode (mode == -2.0): sample texture
|
||||||
return tex.sample(s, in.uv) * in.color;
|
return tex.sample(s, in.uv) * in.color;
|
||||||
} else if (mode < 0.0) {
|
} else if (mode < 0.0) {
|
||||||
// Text mode (mode == -1.0): the glyph atlas stores R8 alpha
|
// Text mode (mode == -1.0): the glyph atlas stores R8 alpha coverage.
|
||||||
// coverage from stbtt_MakeGlyphBitmap. Use the sampled value
|
|
||||||
// directly as alpha (no smoothstep — those were for SDFs and
|
|
||||||
// thinned anti-aliased coverage strokes). Small-size text renders
|
|
||||||
// dim on dark backgrounds because most glyph pixels sit in 0.1-0.5
|
|
||||||
// coverage; tracked as the "faint text" follow-up.
|
|
||||||
float alpha = tex.sample(s, in.uv).r;
|
float alpha = tex.sample(s, in.uv).r;
|
||||||
return float4(in.color.rgb, in.color.a * alpha);
|
return float4(in.color.rgb, in.color.a * alpha);
|
||||||
} else if (mode > 0.0 || border > 0.0) {
|
} else if (mode > 0.0 || border > 0.0) {
|
||||||
|
|||||||
66
specs.md
66
specs.md
@@ -475,6 +475,72 @@ The impl is instantiated per concrete type argument, like generic struct methods
|
|||||||
|
|
||||||
Static dispatch is automatic when the concrete type is known. Dynamic dispatch only when explicitly type-erased via `xx` into a protocol value.
|
Static dispatch is automatic when the concrete type is known. Dynamic dispatch only when explicitly type-erased via `xx` into a protocol value.
|
||||||
|
|
||||||
|
#### Parameterised Protocols (compile-time only)
|
||||||
|
|
||||||
|
A protocol with type parameters is compile-time only — it has no vtable
|
||||||
|
and no boxed instance shape. Each `impl` is monomorphised per
|
||||||
|
`(ProtocolArgs, Source)` pair. The canonical example is `Into`, declared
|
||||||
|
in `modules/std.sx`:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
Into :: protocol(Target: Type) {
|
||||||
|
convert :: () -> Target;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A user can then add conversions for any `(Source, Target)` pair:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
MyString :: struct { tag: s64 = 0; }
|
||||||
|
|
||||||
|
impl Into(MyString) for s64 {
|
||||||
|
convert :: (self: s64) -> MyString { .{ tag = self }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
x : MyString = xx 42; // direct call to monomorphised convert
|
||||||
|
0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `xx` operator hooks into this mechanism: when an explicit target type
|
||||||
|
is provided and the built-in coercion ladder doesn't apply,
|
||||||
|
`xx val : T` lowers to `val.convert()` where `convert` comes from the
|
||||||
|
visible `impl Into(T) for typeof(val)`. The call is a direct call — no
|
||||||
|
vtable, no runtime dispatch.
|
||||||
|
|
||||||
|
**Source side is a TypeExpr.** Unlike nullary `impl P for SomeStruct`,
|
||||||
|
the `for`-side of a parameterised impl accepts any type expression,
|
||||||
|
including closure and function types:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
impl Into(Block) for Closure() -> void { ... }
|
||||||
|
impl Into(MyBuf) for []u8 { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lookup rules:**
|
||||||
|
- **Built-ins win.** The user-space fallback only fires when
|
||||||
|
`coerceToType` made no progress (numeric narrow/widen, ptr↔int, etc.
|
||||||
|
take priority).
|
||||||
|
- **Only at explicit `xx`.** Implicit conversions (assignment,
|
||||||
|
parameter passing) never trigger user-space coercions.
|
||||||
|
- **Explicit target required.** `xx val` with no surrounding type
|
||||||
|
context still defaults to `s64` for legacy reasons; the user-space
|
||||||
|
fallback only fires when the target was named explicitly.
|
||||||
|
- **Import-scoped visibility.** An `impl` is visible from a file only
|
||||||
|
if the file transitively imports the impl's defining module. An impl
|
||||||
|
in an imported-but-not-directly-related module produces a clean
|
||||||
|
diagnostic (`no visible xx conversion …`).
|
||||||
|
- **Duplicate impls error.** If two impls for the same
|
||||||
|
`(Source, Target)` pair are both visible, the compiler emits a
|
||||||
|
diagnostic naming both source modules. Same-file duplicates are
|
||||||
|
caught at registration time. Cross-module duplicates are caught at
|
||||||
|
the `xx` site.
|
||||||
|
- **No recursion.** A `convert` body that re-enters `xx self : Target`
|
||||||
|
for the same `(Source, Target)` pair produces a "recursive xx
|
||||||
|
conversion" diagnostic; the compiler does not try to monomorphise
|
||||||
|
the convert into itself.
|
||||||
|
|
||||||
### Tuple Types
|
### Tuple Types
|
||||||
Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned.
|
Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned.
|
||||||
|
|
||||||
|
|||||||
@@ -241,6 +241,9 @@ pub const VarDecl = struct {
|
|||||||
name: []const u8,
|
name: []const u8,
|
||||||
type_annotation: ?*Node,
|
type_annotation: ?*Node,
|
||||||
value: ?*Node,
|
value: ?*Node,
|
||||||
|
is_foreign: bool = false,
|
||||||
|
foreign_lib: ?[]const u8 = null,
|
||||||
|
foreign_name: ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Assignment = struct {
|
pub const Assignment = struct {
|
||||||
@@ -506,6 +509,7 @@ pub const ProtocolDecl = struct {
|
|||||||
name: []const u8,
|
name: []const u8,
|
||||||
methods: []const ProtocolMethodDecl,
|
methods: []const ProtocolMethodDecl,
|
||||||
is_inline: bool = false, // #inline — embedded fn ptrs instead of vtable pointer
|
is_inline: bool = false, // #inline — embedded fn ptrs instead of vtable pointer
|
||||||
|
type_params: []const StructTypeParam = &.{}, // for `protocol(Target: Type) { ... }`
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ImplBlock = struct {
|
pub const ImplBlock = struct {
|
||||||
@@ -513,4 +517,6 @@ pub const ImplBlock = struct {
|
|||||||
target_type: []const u8,
|
target_type: []const u8,
|
||||||
target_type_params: []const StructTypeParam = &.{}, // for `impl P for List($T)`
|
target_type_params: []const StructTypeParam = &.{}, // for `impl P for List($T)`
|
||||||
methods: []const *Node, // fn_decl nodes
|
methods: []const *Node, // fn_decl nodes
|
||||||
|
protocol_type_args: []const *Node = &.{}, // for `impl Into(Block) for Source` — type args on the protocol side
|
||||||
|
target_type_expr: ?*Node = null, // populated for parameterised-protocol impls; carries non-identifier source spellings (e.g. `Closure() -> void`)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub const Compilation = struct {
|
|||||||
resolved_root: ?*Node = null,
|
resolved_root: ?*Node = null,
|
||||||
import_sources: std.StringHashMap([:0]const u8),
|
import_sources: std.StringHashMap([:0]const u8),
|
||||||
module_scopes: std.StringHashMap(std.StringHashMap(void)),
|
module_scopes: std.StringHashMap(std.StringHashMap(void)),
|
||||||
|
import_graph: std.StringHashMap(std.StringHashMap(void)),
|
||||||
sema_result: ?sema.SemaResult = null,
|
sema_result: ?sema.SemaResult = null,
|
||||||
ir_emitter: ?ir.LLVMEmitter = null,
|
ir_emitter: ?ir.LLVMEmitter = null,
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ pub const Compilation = struct {
|
|||||||
.diagnostics = errors.DiagnosticList.init(allocator, source, file_path),
|
.diagnostics = errors.DiagnosticList.init(allocator, source, file_path),
|
||||||
.import_sources = std.StringHashMap([:0]const u8).init(allocator),
|
.import_sources = std.StringHashMap([:0]const u8).init(allocator),
|
||||||
.module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
.module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||||
|
.import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||||
.target_config = target_config,
|
.target_config = target_config,
|
||||||
.stdlib_paths = stdlib_paths,
|
.stdlib_paths = stdlib_paths,
|
||||||
};
|
};
|
||||||
@@ -69,6 +71,7 @@ pub const Compilation = struct {
|
|||||||
&self.import_sources,
|
&self.import_sources,
|
||||||
&self.diagnostics,
|
&self.diagnostics,
|
||||||
self.stdlib_paths,
|
self.stdlib_paths,
|
||||||
|
&self.import_graph,
|
||||||
) catch return error.CompileError;
|
) catch return error.CompileError;
|
||||||
|
|
||||||
// Preserve per-module visibility scopes for C import access checking
|
// Preserve per-module visibility scopes for C import access checking
|
||||||
@@ -162,6 +165,7 @@ pub const Compilation = struct {
|
|||||||
lowering.target_config = self.target_config;
|
lowering.target_config = self.target_config;
|
||||||
lowering.diagnostics = &self.diagnostics;
|
lowering.diagnostics = &self.diagnostics;
|
||||||
lowering.module_scopes = &self.module_scopes;
|
lowering.module_scopes = &self.module_scopes;
|
||||||
|
lowering.import_graph = &self.import_graph;
|
||||||
lowering.lowerRoot(root);
|
lowering.lowerRoot(root);
|
||||||
if (self.diagnostics.hasErrors()) return error.CompileError;
|
if (self.diagnostics.hasErrors()) return error.CompileError;
|
||||||
return module;
|
return module;
|
||||||
|
|||||||
@@ -175,7 +175,16 @@ pub fn resolveImports(
|
|||||||
source_map: ?*std.StringHashMap([:0]const u8),
|
source_map: ?*std.StringHashMap([:0]const u8),
|
||||||
diagnostics: ?*errors.DiagnosticList,
|
diagnostics: ?*errors.DiagnosticList,
|
||||||
stdlib_paths: []const []const u8,
|
stdlib_paths: []const []const u8,
|
||||||
|
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||||
) !ResolvedModule {
|
) !ResolvedModule {
|
||||||
|
// Record this file's edge set so `param_impl_map` lookups can filter
|
||||||
|
// candidates by what's been imported from where. Populated as each
|
||||||
|
// import resolves below; transitive closure computed on demand.
|
||||||
|
if (import_graph) |g| {
|
||||||
|
if (!g.contains(file_path)) {
|
||||||
|
try g.put(file_path, std.StringHashMap(void).init(allocator));
|
||||||
|
}
|
||||||
|
}
|
||||||
var mod = ResolvedModule{
|
var mod = ResolvedModule{
|
||||||
.path = file_path,
|
.path = file_path,
|
||||||
.decls = &.{},
|
.decls = &.{},
|
||||||
@@ -246,6 +255,15 @@ pub fn resolveImports(
|
|||||||
|
|
||||||
const resolved_path = try resolveImportPath(allocator, io, base_dir, imp.path, null, stdlib_paths);
|
const resolved_path = try resolveImportPath(allocator, io, base_dir, imp.path, null, stdlib_paths);
|
||||||
|
|
||||||
|
// Record direct-import edge file_path → resolved_path. Self-imports
|
||||||
|
// and chain duplicates are still recorded so the graph reflects what
|
||||||
|
// the user wrote (filter happens at lookup).
|
||||||
|
if (import_graph) |g| {
|
||||||
|
if (g.getPtr(file_path)) |set| {
|
||||||
|
set.put(resolved_path, {}) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Circular import check — only along the current chain
|
// Circular import check — only along the current chain
|
||||||
if (chain.contains(resolved_path)) continue;
|
if (chain.contains(resolved_path)) continue;
|
||||||
|
|
||||||
@@ -272,7 +290,7 @@ pub fn resolveImports(
|
|||||||
// Push onto chain before recursing, pop after
|
// Push onto chain before recursing, pop after
|
||||||
try chain.put(resolved_path, {});
|
try chain.put(resolved_path, {});
|
||||||
const imp_dir = dirName(resolved_path);
|
const imp_dir = dirName(resolved_path);
|
||||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths);
|
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph);
|
||||||
_ = chain.remove(resolved_path);
|
_ = chain.remove(resolved_path);
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
@@ -280,7 +298,7 @@ pub fn resolveImports(
|
|||||||
break :blk result;
|
break :blk result;
|
||||||
} else |_| {
|
} else |_| {
|
||||||
// File read failed — try as directory import
|
// File read failed — try as directory import
|
||||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths) catch {
|
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph) catch {
|
||||||
if (diagnostics) |diags| {
|
if (diagnostics) |diags| {
|
||||||
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
|
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
|
||||||
}
|
}
|
||||||
@@ -313,6 +331,7 @@ fn resolveDirectoryImport(
|
|||||||
diagnostics: ?*errors.DiagnosticList,
|
diagnostics: ?*errors.DiagnosticList,
|
||||||
span: ast.Span,
|
span: ast.Span,
|
||||||
stdlib_paths: []const []const u8,
|
stdlib_paths: []const []const u8,
|
||||||
|
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||||
) anyerror!ResolvedModule {
|
) anyerror!ResolvedModule {
|
||||||
// Open the directory with iteration capability
|
// Open the directory with iteration capability
|
||||||
const dir = std.Io.Dir.openDir(.cwd(), io, dir_path, .{ .iterate = true }) catch {
|
const dir = std.Io.Dir.openDir(.cwd(), io, dir_path, .{ .iterate = true }) catch {
|
||||||
@@ -378,7 +397,7 @@ fn resolveDirectoryImport(
|
|||||||
};
|
};
|
||||||
|
|
||||||
try chain.put(file_path, {});
|
try chain.put(file_path, {});
|
||||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths);
|
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph);
|
||||||
_ = chain.remove(file_path);
|
_ = chain.remove(file_path);
|
||||||
|
|
||||||
try cache.put(file_path, result);
|
try cache.put(file_path, result);
|
||||||
|
|||||||
@@ -267,6 +267,15 @@ pub const LLVMEmitter = struct {
|
|||||||
defer self.alloc.free(name_z);
|
defer self.alloc.free(name_z);
|
||||||
|
|
||||||
const llvm_global = c.LLVMAddGlobal(self.llvm_module, llvm_ty, name_z.ptr);
|
const llvm_global = c.LLVMAddGlobal(self.llvm_module, llvm_ty, name_z.ptr);
|
||||||
|
|
||||||
|
// Extern globals (`<name> : <type> #foreign;`) resolve at link time
|
||||||
|
// to a libSystem / framework symbol — no initializer, default linkage.
|
||||||
|
if (global.is_extern) {
|
||||||
|
c.LLVMSetLinkage(llvm_global, c.LLVMExternalLinkage);
|
||||||
|
self.global_map.put(@intCast(i), llvm_global) catch {};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
c.LLVMSetLinkage(llvm_global, c.LLVMInternalLinkage);
|
c.LLVMSetLinkage(llvm_global, c.LLVMInternalLinkage);
|
||||||
|
|
||||||
// Evaluate comptime initializer if present
|
// Evaluate comptime initializer if present
|
||||||
|
|||||||
387
src/ir/lower.zig
387
src/ir/lower.zig
@@ -83,6 +83,7 @@ pub const Lowering = struct {
|
|||||||
local_fn_counter: u32 = 0, // unique counter for mangling local function names
|
local_fn_counter: u32 = 0, // unique counter for mangling local function names
|
||||||
import_flags: std.StringHashMap(bool), // tracks whether each function is imported
|
import_flags: std.StringHashMap(bool), // tracks whether each function is imported
|
||||||
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
|
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
|
||||||
|
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
|
||||||
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
||||||
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
||||||
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
||||||
@@ -103,6 +104,7 @@ pub const Lowering = struct {
|
|||||||
protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds
|
protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds
|
||||||
protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId
|
protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId
|
||||||
protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId
|
protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId
|
||||||
|
param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00<arg_mangled>\x00<src_mangled>" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap)
|
||||||
struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info
|
struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info
|
||||||
module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2)
|
module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2)
|
||||||
foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames
|
foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames
|
||||||
@@ -111,6 +113,7 @@ pub const Lowering = struct {
|
|||||||
target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if)
|
target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if)
|
||||||
comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH)
|
comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH)
|
||||||
diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations
|
diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations
|
||||||
|
xx_reentrancy: std.AutoHashMap(u64, void) = std.AutoHashMap(u64, void).init(std.heap.page_allocator), // (src_ty, dst_ty) pairs currently being resolved through user-space Into; prevents infinite monomorphisation when a convert body re-enters the same xx
|
||||||
|
|
||||||
pub const ComptimeValue = union(enum) {
|
pub const ComptimeValue = union(enum) {
|
||||||
int_val: i64,
|
int_val: i64,
|
||||||
@@ -139,6 +142,17 @@ pub const Lowering = struct {
|
|||||||
ret_type: TypeId,
|
ret_type: TypeId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
|
||||||
|
/// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled).
|
||||||
|
/// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics.
|
||||||
|
const ParamImplEntry = struct {
|
||||||
|
methods: []const *const ast.FnDecl,
|
||||||
|
source_ty: TypeId,
|
||||||
|
target_args: []const TypeId,
|
||||||
|
defining_module: []const u8,
|
||||||
|
span: ast.Span,
|
||||||
|
};
|
||||||
|
|
||||||
/// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports)
|
/// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports)
|
||||||
const StructTemplate = struct {
|
const StructTemplate = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
@@ -294,11 +308,11 @@ pub const Lowering = struct {
|
|||||||
.union_decl => {
|
.union_decl => {
|
||||||
_ = type_bridge.resolveAstType(decl, &self.module.types);
|
_ = type_bridge.resolveAstType(decl, &self.module.types);
|
||||||
},
|
},
|
||||||
.protocol_decl => |pd| {
|
.protocol_decl => {
|
||||||
self.registerProtocolDecl(&pd);
|
self.registerProtocolDecl(&decl.data.protocol_decl);
|
||||||
},
|
},
|
||||||
.impl_block => |ib| {
|
.impl_block => {
|
||||||
self.registerImplBlock(&ib, is_imported);
|
self.registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
||||||
},
|
},
|
||||||
.namespace_decl => |ns| {
|
.namespace_decl => |ns| {
|
||||||
if (self.main_file != null) {
|
if (self.main_file != null) {
|
||||||
@@ -431,11 +445,11 @@ pub const Lowering = struct {
|
|||||||
// Register plain union types in the type table
|
// Register plain union types in the type table
|
||||||
_ = type_bridge.resolveAstType(decl, &self.module.types);
|
_ = type_bridge.resolveAstType(decl, &self.module.types);
|
||||||
},
|
},
|
||||||
.protocol_decl => |pd| {
|
.protocol_decl => {
|
||||||
self.registerProtocolDecl(&pd);
|
self.registerProtocolDecl(&decl.data.protocol_decl);
|
||||||
},
|
},
|
||||||
.impl_block => |ib| {
|
.impl_block => {
|
||||||
self.registerImplBlock(&ib, is_imported);
|
self.registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
||||||
},
|
},
|
||||||
.namespace_decl => |ns| {
|
.namespace_decl => |ns| {
|
||||||
if (self.main_file != null) {
|
if (self.main_file != null) {
|
||||||
@@ -450,8 +464,12 @@ pub const Lowering = struct {
|
|||||||
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
|
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
|
||||||
// to their target type (not a synthetic empty struct).
|
// to their target type (not a synthetic empty struct).
|
||||||
const var_ty = self.resolveType(vd.type_annotation);
|
const var_ty = self.resolveType(vd.type_annotation);
|
||||||
const name_id = self.module.types.internString(vd.name);
|
// Foreign globals reference a symbol defined in libSystem etc.
|
||||||
const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) {
|
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
|
||||||
|
// name is the optional override or the sx name itself.
|
||||||
|
const sym_name = vd.foreign_name orelse vd.name;
|
||||||
|
const name_id = self.module.types.internString(sym_name);
|
||||||
|
const init_val: ?inst_mod.ConstantValue = if (vd.is_foreign) null else if (vd.value) |v| switch (v.data) {
|
||||||
.undef_literal => .zeroinit,
|
.undef_literal => .zeroinit,
|
||||||
.int_literal => |il| .{ .int = il.value },
|
.int_literal => |il| .{ .int = il.value },
|
||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
@@ -466,6 +484,7 @@ pub const Lowering = struct {
|
|||||||
.ty = var_ty,
|
.ty = var_ty,
|
||||||
.init_val = init_val,
|
.init_val = init_val,
|
||||||
.is_const = false,
|
.is_const = false,
|
||||||
|
.is_extern = vd.is_foreign,
|
||||||
});
|
});
|
||||||
self.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {};
|
self.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {};
|
||||||
},
|
},
|
||||||
@@ -5976,14 +5995,19 @@ pub const Lowering = struct {
|
|||||||
const saved_bindings = self.type_bindings;
|
const saved_bindings = self.type_bindings;
|
||||||
const saved_defer_base = self.func_defer_base;
|
const saved_defer_base = self.func_defer_base;
|
||||||
const saved_block_terminated = self.block_terminated;
|
const saved_block_terminated = self.block_terminated;
|
||||||
|
const saved_target = self.target_type;
|
||||||
self.func_defer_base = self.defer_stack.items.len;
|
self.func_defer_base = self.defer_stack.items.len;
|
||||||
self.block_terminated = false;
|
self.block_terminated = false;
|
||||||
|
|
||||||
// Install type bindings
|
// Install type bindings
|
||||||
self.type_bindings = bindings.*;
|
self.type_bindings = bindings.*;
|
||||||
|
|
||||||
// Resolve return type with type bindings active
|
// Resolve return type with type bindings active. The body's tail
|
||||||
|
// expression inherits this as its target_type so bare `.{...}`
|
||||||
|
// literals resolve to the monomorphised return type instead of
|
||||||
|
// whatever leaked in from the caller (e.g. caller's xx target).
|
||||||
const ret_ty = self.resolveReturnType(fd);
|
const ret_ty = self.resolveReturnType(fd);
|
||||||
|
self.target_type = ret_ty;
|
||||||
|
|
||||||
// Build param list (substituting type params, skipping type param declarations)
|
// Build param list (substituting type params, skipping type param declarations)
|
||||||
var params = std.ArrayList(Function.Param).empty;
|
var params = std.ArrayList(Function.Param).empty;
|
||||||
@@ -6060,6 +6084,7 @@ pub const Lowering = struct {
|
|||||||
self.scope = saved_scope;
|
self.scope = saved_scope;
|
||||||
self.func_defer_base = saved_defer_base;
|
self.func_defer_base = saved_defer_base;
|
||||||
self.block_terminated = saved_block_terminated;
|
self.block_terminated = saved_block_terminated;
|
||||||
|
self.target_type = saved_target;
|
||||||
self.builder.func = saved_func;
|
self.builder.func = saved_func;
|
||||||
self.builder.current_block = saved_block;
|
self.builder.current_block = saved_block;
|
||||||
self.builder.inst_counter = saved_counter;
|
self.builder.inst_counter = saved_counter;
|
||||||
@@ -6435,10 +6460,74 @@ pub const Lowering = struct {
|
|||||||
const inner = self.mangleTypeName(v.element);
|
const inner = self.mangleTypeName(v.element);
|
||||||
break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector";
|
break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector";
|
||||||
},
|
},
|
||||||
|
.closure => |c| self.mangleParamList("cl", c.params, c.ret),
|
||||||
|
.function => |f| self.mangleParamList("fn", f.params, f.ret),
|
||||||
|
.tuple => |t| blk: {
|
||||||
|
var buf = std.ArrayList(u8).empty;
|
||||||
|
buf.appendSlice(self.alloc, "tu") catch break :blk "tuple";
|
||||||
|
for (t.fields) |fid| {
|
||||||
|
buf.append(self.alloc, '_') catch break :blk "tuple";
|
||||||
|
buf.appendSlice(self.alloc, self.mangleTypeName(fid)) catch break :blk "tuple";
|
||||||
|
}
|
||||||
|
break :blk buf.items;
|
||||||
|
},
|
||||||
else => @tagName(info),
|
else => @tagName(info),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect impl entries visible from `current_source_file` — defined in
|
||||||
|
/// the current file or in any module the current file transitively
|
||||||
|
/// imports. Falls open (returns all entries) when the source-file
|
||||||
|
/// context or import graph isn't wired (e.g. comptime callers).
|
||||||
|
fn findVisibleImpls(self: *Lowering, entries: []const ParamImplEntry, out: *std.ArrayList(ParamImplEntry)) void {
|
||||||
|
const here = self.current_source_file orelse {
|
||||||
|
out.appendSlice(self.alloc, entries) catch {};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const graph = self.import_graph orelse {
|
||||||
|
out.appendSlice(self.alloc, entries) catch {};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// BFS over the import graph to compute the visible set.
|
||||||
|
var visible = std.StringHashMap(void).init(self.alloc);
|
||||||
|
defer visible.deinit();
|
||||||
|
visible.put(here, {}) catch {};
|
||||||
|
var queue = std.ArrayList([]const u8).empty;
|
||||||
|
defer queue.deinit(self.alloc);
|
||||||
|
queue.append(self.alloc, here) catch {};
|
||||||
|
var head: usize = 0;
|
||||||
|
while (head < queue.items.len) : (head += 1) {
|
||||||
|
const node = queue.items[head];
|
||||||
|
const direct = graph.get(node) orelse continue;
|
||||||
|
var it = direct.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
const next = kv.key_ptr.*;
|
||||||
|
if (visible.contains(next)) continue;
|
||||||
|
visible.put(next, {}) catch {};
|
||||||
|
queue.append(self.alloc, next) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entries) |e| {
|
||||||
|
if (visible.contains(e.defining_module)) {
|
||||||
|
out.append(self.alloc, e) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mangleParamList(self: *Lowering, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 {
|
||||||
|
var buf = std.ArrayList(u8).empty;
|
||||||
|
buf.appendSlice(self.alloc, prefix) catch return prefix;
|
||||||
|
for (params) |p| {
|
||||||
|
buf.append(self.alloc, '_') catch return prefix;
|
||||||
|
buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return prefix;
|
||||||
|
}
|
||||||
|
buf.appendSlice(self.alloc, "__") catch return prefix;
|
||||||
|
buf.appendSlice(self.alloc, self.mangleTypeName(ret)) catch return prefix;
|
||||||
|
return buf.items;
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
|
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
|
||||||
/// Returns a list of TypeId index values that match the category.
|
/// Returns a list of TypeId index values that match the category.
|
||||||
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
|
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
|
||||||
@@ -6584,6 +6673,27 @@ pub const Lowering = struct {
|
|||||||
if (c.callee.data == .field_access) {
|
if (c.callee.data == .field_access) {
|
||||||
const fa = c.callee.data.field_access;
|
const fa = c.callee.data.field_access;
|
||||||
const obj_ty = self.inferExprType(fa.object);
|
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.
|
||||||
|
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
||||||
|
for (proto_info.methods) |m| {
|
||||||
|
if (std.mem.eql(u8, m.name, fa.field)) return m.param_types;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Closure-typed struct field: `c.on(args)` lowers to call_closure on
|
||||||
|
// the field value. Pick up the callee's param types from the closure
|
||||||
|
// type so each arg gets the right target_type during lowering.
|
||||||
|
if (!obj_ty.isBuiltin()) {
|
||||||
|
const field_name_id = self.module.types.internString(fa.field);
|
||||||
|
const struct_fields = self.getStructFields(obj_ty);
|
||||||
|
for (struct_fields) |f| {
|
||||||
|
if (f.name == field_name_id and !f.ty.isBuiltin()) {
|
||||||
|
const fti = self.module.types.get(f.ty);
|
||||||
|
if (fti == .closure) return fti.closure.params;
|
||||||
|
if (fti == .function) return fti.function.params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (self.getStructTypeName(obj_ty)) |sname| {
|
if (self.getStructTypeName(obj_ty)) |sname| {
|
||||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{};
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{};
|
||||||
// Try already-lowered functions first
|
// Try already-lowered functions first
|
||||||
@@ -7498,6 +7608,16 @@ pub const Lowering = struct {
|
|||||||
/// Non-inline protocols: { ctx: *void, __vtable: *void }
|
/// Non-inline protocols: { ctx: *void, __vtable: *void }
|
||||||
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
|
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
|
||||||
fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
|
fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
|
||||||
|
// Parameterised protocols are compile-time-only — no vtable, no boxed
|
||||||
|
// instance struct. Methods reference unbound type params (e.g.
|
||||||
|
// `convert :: () -> Target`) that only get a concrete TypeId per
|
||||||
|
// (Source, Target) pair at xx resolution time. Stash the AST so
|
||||||
|
// `param_impl_map` lookup can resolve method signatures lazily.
|
||||||
|
if (pd.type_params.len > 0) {
|
||||||
|
self.protocol_ast_map.put(pd.name, pd) catch {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const table = &self.module.types;
|
const table = &self.module.types;
|
||||||
const name_id = table.internString(pd.name);
|
const name_id = table.internString(pd.name);
|
||||||
|
|
||||||
@@ -7596,7 +7716,15 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Register an impl block: register its methods as TypeName.method in fn_ast_map.
|
/// Register an impl block: register its methods as TypeName.method in fn_ast_map.
|
||||||
fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool) void {
|
fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void {
|
||||||
|
// Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`):
|
||||||
|
// record into `param_impl_map` for compile-time resolution by `lowerXX`.
|
||||||
|
// Methods are NOT registered in fn_ast_map — they're monomorphised lazily
|
||||||
|
// per (Source, Target) pair at the xx call site.
|
||||||
|
if (ib.protocol_type_args.len > 0) {
|
||||||
|
self.registerParamImpl(ib, decl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Collect explicitly implemented method names
|
// Collect explicitly implemented method names
|
||||||
var impl_methods = std.StringHashMap(void).init(self.alloc);
|
var impl_methods = std.StringHashMap(void).init(self.alloc);
|
||||||
defer impl_methods.deinit();
|
defer impl_methods.deinit();
|
||||||
@@ -7625,6 +7753,80 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a parameterised-protocol impl into `param_impl_map`.
|
||||||
|
/// Resolves the protocol's type args + the source type, mangles them, and
|
||||||
|
/// stashes the impl's method fn_decls for later monomorphisation by
|
||||||
|
/// `lowerXX`. Same-module duplicate impls produce a diagnostic here;
|
||||||
|
/// cross-module duplicates are detected at the xx resolution site.
|
||||||
|
fn registerParamImpl(self: *Lowering, ib: *const ast.ImplBlock, decl: *const Node) void {
|
||||||
|
const table = &self.module.types;
|
||||||
|
|
||||||
|
// Resolve the protocol's type-arg list to concrete TypeIds.
|
||||||
|
var arg_tys = std.ArrayList(TypeId).empty;
|
||||||
|
for (ib.protocol_type_args) |arg_node| {
|
||||||
|
const t = type_bridge.resolveAstType(arg_node, table);
|
||||||
|
arg_tys.append(self.alloc, t) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the source type. Parser stores it on `target_type_expr` for
|
||||||
|
// parameterised impls (back-compat `target_type` string is kept for
|
||||||
|
// simple cases but the canonical form is the TypeExpr).
|
||||||
|
const src_ty: TypeId = if (ib.target_type_expr) |te|
|
||||||
|
type_bridge.resolveAstType(te, table)
|
||||||
|
else if (ib.target_type.len > 0)
|
||||||
|
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table)
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Mangle into the lookup key.
|
||||||
|
var key_buf = std.ArrayList(u8).empty;
|
||||||
|
key_buf.appendSlice(self.alloc, ib.protocol_name) catch return;
|
||||||
|
for (arg_tys.items) |t| {
|
||||||
|
key_buf.append(self.alloc, 0) catch return;
|
||||||
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return;
|
||||||
|
}
|
||||||
|
key_buf.append(self.alloc, 0) catch return;
|
||||||
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return;
|
||||||
|
const key = key_buf.items;
|
||||||
|
|
||||||
|
// Collect method fn_decl pointers.
|
||||||
|
var methods = std.ArrayList(*const ast.FnDecl).empty;
|
||||||
|
for (ib.methods) |method_node| {
|
||||||
|
if (method_node.data == .fn_decl) {
|
||||||
|
methods.append(self.alloc, &method_node.data.fn_decl) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defining_module: []const u8 = self.current_source_file orelse "";
|
||||||
|
const entry: ParamImplEntry = .{
|
||||||
|
.methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
|
||||||
|
.source_ty = src_ty,
|
||||||
|
.target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return,
|
||||||
|
.defining_module = defining_module,
|
||||||
|
.span = decl.span,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gop = self.param_impl_map.getOrPut(key) catch return;
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = std.ArrayList(ParamImplEntry).empty;
|
||||||
|
} else {
|
||||||
|
// Same-file duplicate is an immediate error. Cross-file overlaps
|
||||||
|
// are deferred to the xx resolution site (Phase 5) so the impl
|
||||||
|
// surface can be richer than any one file's view.
|
||||||
|
for (gop.value_ptr.items) |existing| {
|
||||||
|
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
|
||||||
|
if (self.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{
|
||||||
|
ib.protocol_name, self.mangleTypeName(src_ty), defining_module,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gop.value_ptr.append(self.alloc, entry) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
/// Synthesize a fn_decl from a protocol default method for a concrete type.
|
/// Synthesize a fn_decl from a protocol default method for a concrete type.
|
||||||
fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl {
|
fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl {
|
||||||
// Build parameter list: self: *TargetType, then the protocol method params
|
// Build parameter list: self: *TargetType, then the protocol method params
|
||||||
@@ -7957,15 +8159,15 @@ pub const Lowering = struct {
|
|||||||
const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type);
|
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,
|
// If protocol method returns *void (Self) and the caller expects a value type,
|
||||||
// unbox: load the concrete value from the returned pointer
|
// unbox: load the concrete value from the returned pointer. Real pointer
|
||||||
if (mi.ret_type != .void) {
|
// returns (declared `-> *T` for non-Self T) are NOT auto-loaded — the
|
||||||
const ret_info = self.module.types.get(mi.ret_type);
|
// pointee may be a single byte and reading `sizeof(target)` past it
|
||||||
if (ret_info == .pointer) {
|
// segfaults. Self is encoded as `*void`, so test against that exact type.
|
||||||
if (self.target_type) |target| {
|
if (mi.ret_type == void_ptr) {
|
||||||
const target_info = self.module.types.get(target);
|
if (self.target_type) |target| {
|
||||||
if (target_info != .pointer) {
|
const target_info = self.module.types.get(target);
|
||||||
return self.builder.load(raw_result, target);
|
if (target_info != .pointer) {
|
||||||
}
|
return self.builder.load(raw_result, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8334,7 +8536,13 @@ pub const Lowering = struct {
|
|||||||
/// - int → int: widen/narrow
|
/// - int → int: widen/narrow
|
||||||
/// - int ↔ float: int_to_float/float_to_int
|
/// - int ↔ float: int_to_float/float_to_int
|
||||||
fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref {
|
fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref {
|
||||||
const src_ty = self.inferExprType(operand_node);
|
// Use the operand's *actual* lowered Ref type rather than reaching
|
||||||
|
// back through inferExprType — the latter doesn't cover every
|
||||||
|
// expression shape (notably lambdas), and a wrong src_ty here can
|
||||||
|
// route the cast through coerceToType (e.g. a bogus s64→ptr bitcast)
|
||||||
|
// and silently skip the user-space Into fallback.
|
||||||
|
const src_ty = self.builder.getRefType(operand);
|
||||||
|
const target_explicit = self.target_type != null;
|
||||||
const dst_ty = self.target_type orelse .s64;
|
const dst_ty = self.target_type orelse .s64;
|
||||||
|
|
||||||
// Any → concrete type: unbox
|
// Any → concrete type: unbox
|
||||||
@@ -8377,7 +8585,140 @@ pub const Lowering = struct {
|
|||||||
return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty);
|
return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.coerceToType(operand, src_ty, dst_ty);
|
const result = self.coerceToType(operand, src_ty, dst_ty);
|
||||||
|
|
||||||
|
// User-space fallback via `impl Into(Target) for Source`. Only fires
|
||||||
|
// when the target was explicitly named (not the .s64 default), src and
|
||||||
|
// dst differ, and the built-in ladder made no progress. Built-ins
|
||||||
|
// always win.
|
||||||
|
if (target_explicit and src_ty != dst_ty and result == operand) {
|
||||||
|
if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| {
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
// Pointer-target fallback: `xx <expr>` whose surrounding context
|
||||||
|
// expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate)
|
||||||
|
// can be satisfied by `impl Into(T) for src` plus an implicit
|
||||||
|
// alloca+store on the result. Lets users write
|
||||||
|
// `fn(xx () => { ... })` instead of materialising a named Block local
|
||||||
|
// just to take its address.
|
||||||
|
if (!dst_ty.isBuiltin()) {
|
||||||
|
const dst_info = self.module.types.get(dst_ty);
|
||||||
|
if (dst_info == .pointer) {
|
||||||
|
const pointee = dst_info.pointer.pointee;
|
||||||
|
if (pointee != src_ty) {
|
||||||
|
if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| {
|
||||||
|
const slot = self.builder.alloca(pointee);
|
||||||
|
self.builder.store(slot, converted);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise
|
||||||
|
/// the impl's `convert` method and emit a direct call. Returns null when
|
||||||
|
/// no impl matches (caller falls back to the built-in result, which is
|
||||||
|
/// the unchanged operand — Phase 3 emits no diagnostic for v0).
|
||||||
|
fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref {
|
||||||
|
// Reentrancy guard — pack (src, dst) into a u64.
|
||||||
|
const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index());
|
||||||
|
if (self.xx_reentrancy.contains(guard_key)) {
|
||||||
|
if (self.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{
|
||||||
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return operand;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build lookup key: "Into\x00<dst_mangled>\x00<src_mangled>".
|
||||||
|
// Hardcoded to the "Into" protocol for v1. Generalising to other
|
||||||
|
// parameterised protocols would walk protocol_decl_map looking for
|
||||||
|
// protocols that take a single type-param and have a `convert` method.
|
||||||
|
const proto_name = "Into";
|
||||||
|
const pd = self.protocol_ast_map.get(proto_name) orelse return null;
|
||||||
|
if (pd.type_params.len != 1) return null;
|
||||||
|
|
||||||
|
var key_buf = std.ArrayList(u8).empty;
|
||||||
|
key_buf.appendSlice(self.alloc, proto_name) catch return null;
|
||||||
|
key_buf.append(self.alloc, 0) catch return null;
|
||||||
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null;
|
||||||
|
key_buf.append(self.alloc, 0) catch return null;
|
||||||
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null;
|
||||||
|
const key = key_buf.items;
|
||||||
|
|
||||||
|
const entries = self.param_impl_map.get(key) orelse return null;
|
||||||
|
if (entries.items.len == 0) return null;
|
||||||
|
|
||||||
|
// Filter by import visibility: only impls in modules that the current
|
||||||
|
// file transitively imports (or the current file itself) are reachable.
|
||||||
|
// Falls open when import_graph isn't wired (e.g. comptime callers).
|
||||||
|
var visible_impls = std.ArrayList(ParamImplEntry).empty;
|
||||||
|
defer visible_impls.deinit(self.alloc);
|
||||||
|
self.findVisibleImpls(entries.items, &visible_impls);
|
||||||
|
|
||||||
|
if (visible_impls.items.len == 0) {
|
||||||
|
if (self.diagnostics) |diags| {
|
||||||
|
const saved = diags.current_source_file;
|
||||||
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
||||||
|
defer diags.current_source_file = saved;
|
||||||
|
diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{
|
||||||
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return operand;
|
||||||
|
}
|
||||||
|
if (visible_impls.items.len > 1) {
|
||||||
|
if (self.diagnostics) |diags| {
|
||||||
|
const saved = diags.current_source_file;
|
||||||
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
||||||
|
defer diags.current_source_file = saved;
|
||||||
|
diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{
|
||||||
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
||||||
|
visible_impls.items[0].defining_module, visible_impls.items[1].defining_module,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return operand;
|
||||||
|
}
|
||||||
|
const entry = visible_impls.items[0];
|
||||||
|
|
||||||
|
// Find the `convert` method on this impl.
|
||||||
|
var convert_fd: ?*const ast.FnDecl = null;
|
||||||
|
for (entry.methods) |m| {
|
||||||
|
if (std.mem.eql(u8, m.name, "convert")) {
|
||||||
|
convert_fd = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fd = convert_fd orelse return null;
|
||||||
|
|
||||||
|
// Bind Target → dst_ty.
|
||||||
|
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
||||||
|
defer bindings.deinit();
|
||||||
|
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
|
||||||
|
|
||||||
|
// Mangled name: "<src>.convert__<dst>".
|
||||||
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
|
||||||
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
||||||
|
}) catch return null;
|
||||||
|
|
||||||
|
self.xx_reentrancy.put(guard_key, {}) catch {};
|
||||||
|
defer _ = self.xx_reentrancy.remove(guard_key);
|
||||||
|
|
||||||
|
if (!self.lowered_functions.contains(mangled)) {
|
||||||
|
self.monomorphizeFunction(fd, mangled, &bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fid = self.resolveFuncByName(mangled) orelse return null;
|
||||||
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||||
|
const ret_ty = func.ret;
|
||||||
|
const params = func.params;
|
||||||
|
var args = [_]Ref{operand};
|
||||||
|
self.coerceCallArgs(args[0..], params);
|
||||||
|
return self.builder.call(fid, args[0..], ret_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a protocol value from a concrete value via xx conversion.
|
/// Build a protocol value from a concrete value via xx conversion.
|
||||||
|
|||||||
143
src/parser.zig
143
src/parser.zig
@@ -362,7 +362,32 @@ pub const Parser = struct {
|
|||||||
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = null } });
|
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = null } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.fail("expected ':', '=' or ';' after type annotation");
|
if (self.current.tag == .hash_foreign) {
|
||||||
|
// name : type #foreign [lib] ["c_name"]; (extern global from libsystem etc.)
|
||||||
|
self.advance();
|
||||||
|
var lib_ref: ?[]const u8 = null;
|
||||||
|
if (self.current.tag == .identifier) {
|
||||||
|
lib_ref = self.tokenSlice(self.current);
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
var c_name: ?[]const u8 = null;
|
||||||
|
if (self.current.tag == .string_literal) {
|
||||||
|
const raw = self.tokenSlice(self.current);
|
||||||
|
c_name = raw[1 .. raw.len - 1];
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
try self.expect(.semicolon);
|
||||||
|
return try self.createNode(start_pos, .{ .var_decl = .{
|
||||||
|
.name = name,
|
||||||
|
.type_annotation = type_node,
|
||||||
|
.value = null,
|
||||||
|
.is_foreign = true,
|
||||||
|
.foreign_lib = lib_ref,
|
||||||
|
.foreign_name = c_name,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.fail("expected ':', '=', ';' or '#foreign' after type annotation");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||||
@@ -890,6 +915,29 @@ pub const Parser = struct {
|
|||||||
fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
||||||
self.advance(); // skip 'protocol'
|
self.advance(); // skip 'protocol'
|
||||||
|
|
||||||
|
// Optional type params: protocol(Target: Type, U: Type) { ... }
|
||||||
|
// Names are introduced without a `$` sigil (unlike struct's $T) because
|
||||||
|
// the parens after `protocol` already mark this as a parameter list.
|
||||||
|
var type_params = std.ArrayList(ast.StructTypeParam).empty;
|
||||||
|
if (self.current.tag == .l_paren) {
|
||||||
|
self.advance(); // skip '('
|
||||||
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||||
|
if (type_params.items.len > 0) {
|
||||||
|
try self.expect(.comma);
|
||||||
|
if (self.current.tag == .r_paren) break;
|
||||||
|
}
|
||||||
|
if (self.current.tag != .identifier) {
|
||||||
|
return self.fail("expected type parameter name in protocol header");
|
||||||
|
}
|
||||||
|
const param_name = self.tokenSlice(self.current);
|
||||||
|
self.advance();
|
||||||
|
try self.expect(.colon);
|
||||||
|
const constraint = try self.parseTypeExpr();
|
||||||
|
try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
||||||
|
}
|
||||||
|
try self.expect(.r_paren);
|
||||||
|
}
|
||||||
|
|
||||||
// Check for #inline
|
// Check for #inline
|
||||||
var is_inline = false;
|
var is_inline = false;
|
||||||
if (self.current.tag == .hash_inline) {
|
if (self.current.tag == .hash_inline) {
|
||||||
@@ -899,6 +947,14 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
try self.expect(.l_brace);
|
try self.expect(.l_brace);
|
||||||
|
|
||||||
|
// Push type-param names into scope so method signatures can refer to them
|
||||||
|
// bare (e.g. `convert :: () -> Target` resolves Target as a generic type expr).
|
||||||
|
var tp_names = std.ArrayList([]const u8).empty;
|
||||||
|
for (type_params.items) |tp| try tp_names.append(self.allocator, tp.name);
|
||||||
|
const saved_struct_type_params = self.struct_type_params;
|
||||||
|
self.struct_type_params = tp_names.items;
|
||||||
|
defer self.struct_type_params = saved_struct_type_params;
|
||||||
|
|
||||||
var methods = std.ArrayList(ast.ProtocolMethodDecl).empty;
|
var methods = std.ArrayList(ast.ProtocolMethodDecl).empty;
|
||||||
|
|
||||||
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
||||||
@@ -962,6 +1018,7 @@ pub const Parser = struct {
|
|||||||
.name = name,
|
.name = name,
|
||||||
.methods = try methods.toOwnedSlice(self.allocator),
|
.methods = try methods.toOwnedSlice(self.allocator),
|
||||||
.is_inline = is_inline,
|
.is_inline = is_inline,
|
||||||
|
.type_params = try type_params.toOwnedSlice(self.allocator),
|
||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,39 +1032,71 @@ pub const Parser = struct {
|
|||||||
const protocol_name = self.tokenSlice(self.current);
|
const protocol_name = self.tokenSlice(self.current);
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
|
// Optional protocol type args: impl Into(Block) for ...
|
||||||
|
var protocol_type_args = std.ArrayList(*Node).empty;
|
||||||
|
if (self.current.tag == .l_paren) {
|
||||||
|
self.advance(); // skip '('
|
||||||
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||||
|
if (protocol_type_args.items.len > 0) {
|
||||||
|
try self.expect(.comma);
|
||||||
|
if (self.current.tag == .r_paren) break;
|
||||||
|
}
|
||||||
|
try protocol_type_args.append(self.allocator, try self.parseTypeExpr());
|
||||||
|
}
|
||||||
|
try self.expect(.r_paren);
|
||||||
|
}
|
||||||
|
|
||||||
// 'for' — note: 'for' is a keyword (kw_for), not an identifier
|
// 'for' — note: 'for' is a keyword (kw_for), not an identifier
|
||||||
if (self.current.tag != .kw_for) {
|
if (self.current.tag != .kw_for) {
|
||||||
return self.fail("expected 'for' after protocol name in impl block");
|
return self.fail("expected 'for' after protocol name in impl block");
|
||||||
}
|
}
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
// Target type name (identifiers like s64, or keywords like f32/f64)
|
// Source-type spelling. For parameterised protocols we accept any TypeExpr
|
||||||
if (self.current.tag != .identifier and !self.current.tag.isTypeKeyword()) {
|
// (`Closure(...) -> R`, `*T`, etc.). For nullary protocols we keep the
|
||||||
return self.fail("expected type name after 'for'");
|
// legacy identifier-only path so existing `impl P for SomeStruct` keeps
|
||||||
}
|
// working unchanged (the parser doesn't try to over-parse trailing tokens).
|
||||||
const target_type = self.tokenSlice(self.current);
|
var target_type: []const u8 = "";
|
||||||
self.advance();
|
var target_type_expr: ?*Node = null;
|
||||||
|
|
||||||
// Optional type params: impl Protocol for List($T)
|
|
||||||
var target_type_params = std.ArrayList(ast.StructTypeParam).empty;
|
var target_type_params = std.ArrayList(ast.StructTypeParam).empty;
|
||||||
if (self.current.tag == .l_paren) {
|
|
||||||
self.advance(); // skip '('
|
if (protocol_type_args.items.len > 0) {
|
||||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
// Parameterised protocol — source is a general TypeExpr.
|
||||||
if (target_type_params.items.len > 0) {
|
target_type_expr = try self.parseTypeExpr();
|
||||||
try self.expect(.comma);
|
// Synthesize a string view of the source for back-compat consumers
|
||||||
if (self.current.tag == .r_paren) break;
|
// (LSP hover, etc.). The semantic key for the impl map uses
|
||||||
}
|
// structural mangling, not this string.
|
||||||
try self.expect(.dollar);
|
if (target_type_expr.?.data == .type_expr) {
|
||||||
if (self.current.tag != .identifier) {
|
target_type = target_type_expr.?.data.type_expr.name;
|
||||||
return self.fail("expected type parameter name after '$'");
|
}
|
||||||
}
|
} else {
|
||||||
const param_name = self.tokenSlice(self.current);
|
// Legacy nullary-protocol path: single identifier source.
|
||||||
self.advance();
|
if (self.current.tag != .identifier and !self.current.tag.isTypeKeyword()) {
|
||||||
// Optional constraint — for now just use Type
|
return self.fail("expected type name after 'for'");
|
||||||
const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } });
|
}
|
||||||
try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
target_type = self.tokenSlice(self.current);
|
||||||
|
self.advance();
|
||||||
|
|
||||||
|
// Optional type params: impl Protocol for List($T)
|
||||||
|
if (self.current.tag == .l_paren) {
|
||||||
|
self.advance(); // skip '('
|
||||||
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||||
|
if (target_type_params.items.len > 0) {
|
||||||
|
try self.expect(.comma);
|
||||||
|
if (self.current.tag == .r_paren) break;
|
||||||
|
}
|
||||||
|
try self.expect(.dollar);
|
||||||
|
if (self.current.tag != .identifier) {
|
||||||
|
return self.fail("expected type parameter name after '$'");
|
||||||
|
}
|
||||||
|
const param_name = self.tokenSlice(self.current);
|
||||||
|
self.advance();
|
||||||
|
// Optional constraint — for now just use Type
|
||||||
|
const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } });
|
||||||
|
try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
||||||
|
}
|
||||||
|
try self.expect(.r_paren);
|
||||||
}
|
}
|
||||||
try self.expect(.r_paren);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.expect(.l_brace);
|
try self.expect(.l_brace);
|
||||||
@@ -1045,6 +1134,8 @@ pub const Parser = struct {
|
|||||||
.target_type = target_type,
|
.target_type = target_type,
|
||||||
.target_type_params = try target_type_params.toOwnedSlice(self.allocator),
|
.target_type_params = try target_type_params.toOwnedSlice(self.allocator),
|
||||||
.methods = try methods.toOwnedSlice(self.allocator),
|
.methods = try methods.toOwnedSlice(self.allocator),
|
||||||
|
.protocol_type_args = try protocol_type_args.toOwnedSlice(self.allocator),
|
||||||
|
.target_type_expr = target_type_expr,
|
||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
tests/expected/88-enum-through-protocol-dispatch.exit
Normal file
1
tests/expected/88-enum-through-protocol-dispatch.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
3
tests/expected/88-enum-through-protocol-dispatch.txt
Normal file
3
tests/expected/88-enum-through-protocol-dispatch.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
direct a=0 b=1
|
||||||
|
proto f = 1
|
||||||
|
proto f = 0
|
||||||
1
tests/expected/89-enum-arg-through-closure-field.exit
Normal file
1
tests/expected/89-enum-arg-through-closure-field.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/89-enum-arg-through-closure-field.txt
Normal file
2
tests/expected/89-enum-arg-through-closure-field.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
cl f = 1
|
||||||
|
cl f = 0
|
||||||
1
tests/expected/90-protocol-real-pointer-return.exit
Normal file
1
tests/expected/90-protocol-real-pointer-return.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/90-protocol-real-pointer-return.txt
Normal file
1
tests/expected/90-protocol-real-pointer-return.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
got pointer: true
|
||||||
1
tests/expected/91-protocol-typeparam-parse.exit
Normal file
1
tests/expected/91-protocol-typeparam-parse.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/91-protocol-typeparam-parse.txt
Normal file
1
tests/expected/91-protocol-typeparam-parse.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
1
tests/expected/92-xx-userspace.exit
Normal file
1
tests/expected/92-xx-userspace.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/92-xx-userspace.txt
Normal file
1
tests/expected/92-xx-userspace.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tag = 42
|
||||||
1
tests/expected/93-into-import-scope.exit
Normal file
1
tests/expected/93-into-import-scope.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/93-into-import-scope.txt
Normal file
1
tests/expected/93-into-import-scope.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
w.v = 30
|
||||||
1
tests/expected/94-foreign-global.exit
Normal file
1
tests/expected/94-foreign-global.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/94-foreign-global.txt
Normal file
1
tests/expected/94-foreign-global.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
stdin extern global non-null: true
|
||||||
1
tests/expected/95-objc-block-noop.exit
Normal file
1
tests/expected/95-objc-block-noop.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/95-objc-block-noop.txt
Normal file
1
tests/expected/95-objc-block-noop.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
noop block ran
|
||||||
1
tests/expected/96-objc-block-capture.exit
Normal file
1
tests/expected/96-objc-block-capture.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/96-objc-block-capture.txt
Normal file
1
tests/expected/96-objc-block-capture.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
x + y = 142
|
||||||
1
tests/expected/97-objc-block-inline.exit
Normal file
1
tests/expected/97-objc-block-inline.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/97-objc-block-inline.txt
Normal file
1
tests/expected/97-objc-block-inline.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
inline block, x = 7
|
||||||
1
tests/expected/issue-0032.exit
Normal file
1
tests/expected/issue-0032.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
1
tests/expected/issue-0032.txt
Normal file
1
tests/expected/issue-0032.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/Users/agra/projects/sx/examples/issue-0032.sx:13:1: error: duplicate impl 'Into' for source 's64' in /Users/agra/projects/sx/examples/issue-0032.sx
|
||||||
1
tests/expected/issue-0033.exit
Normal file
1
tests/expected/issue-0033.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
1
tests/expected/issue-0033.txt
Normal file
1
tests/expected/issue-0033.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/Users/agra/projects/sx/examples/./issue-0033-user.sx:7:17: error: no visible xx conversion from 's64' to 'Wrap' — impl exists in another module but is not imported
|
||||||
1
tests/expected/issue-0034.exit
Normal file
1
tests/expected/issue-0034.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
1
tests/expected/issue-0034.txt
Normal file
1
tests/expected/issue-0034.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/Users/agra/projects/sx/examples/issue-0034.sx:11:17: error: duplicate xx conversion from 's64' to 'Wrap': impls in /Users/agra/projects/sx/examples/./issue-0034-impl-a.sx and /Users/agra/projects/sx/examples/./issue-0034-impl-b.sx
|
||||||
50
wasm_check.html
Normal file
50
wasm_check.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
|
<title>sx</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{width:100%;height:100%;overflow:hidden;background:#1e1e24}
|
||||||
|
canvas{display:block;width:100vw;height:100vh;outline:none}
|
||||||
|
#overlay{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1e1e24;z-index:10;transition:opacity .4s}
|
||||||
|
#overlay.hidden{opacity:0;pointer-events:none}
|
||||||
|
#overlay .bar-track{width:min(280px,60vw);height:3px;background:#2a2a32;border-radius:2px;margin-top:18px}
|
||||||
|
#overlay .bar-fill{height:100%;width:0%;background:#7c7cff;border-radius:2px;transition:width .15s}
|
||||||
|
#overlay .status{color:#888;font:13px/1 -apple-system,system-ui,sans-serif;margin-top:10px;letter-spacing:.02em}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="overlay">
|
||||||
|
<div class="bar-track"><div class="bar-fill" id="bar"></div></div>
|
||||||
|
<div class="status" id="status">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
|
||||||
|
<script>
|
||||||
|
var bar=document.getElementById('bar');
|
||||||
|
var status=document.getElementById('status');
|
||||||
|
var overlay=document.getElementById('overlay');
|
||||||
|
var Module={
|
||||||
|
canvas:document.getElementById('canvas'),
|
||||||
|
print:function(){console.log(Array.prototype.slice.call(arguments).join(' '))},
|
||||||
|
printErr:function(){console.warn(Array.prototype.slice.call(arguments).join(' '))},
|
||||||
|
setStatus:function(t){
|
||||||
|
if(!t){overlay.classList.add('hidden');return}
|
||||||
|
var m=t.match(/\((\d+(?:\.\d+)?)\/(\d+)\)/);
|
||||||
|
if(m){bar.style.width=(parseInt(m[1])/parseInt(m[2])*100)+'%';status.textContent='Loading\u2026'}
|
||||||
|
else{status.textContent=t}
|
||||||
|
},
|
||||||
|
totalDependencies:0,
|
||||||
|
monitorRunDependencies:function(left){
|
||||||
|
this.totalDependencies=Math.max(this.totalDependencies,left);
|
||||||
|
this.setStatus(left?'Loading... ('+( this.totalDependencies-left)+'/'+this.totalDependencies+')':'');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Module.setStatus('Loading\u2026');
|
||||||
|
window.onerror=function(){Module.setStatus('Error — see console');};
|
||||||
|
</script>
|
||||||
|
<script>Module.locateFile=function(p){return p+'?v=3efbbd82'}</script>
|
||||||
|
<script async type="text/javascript" src="wasm_check.js?v=3efbbd82"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1765
wasm_check.js
Normal file
1765
wasm_check.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
wasm_check.wasm
Executable file
BIN
wasm_check.wasm
Executable file
Binary file not shown.
50
wasm_check2.html
Normal file
50
wasm_check2.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
|
<title>sx</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{width:100%;height:100%;overflow:hidden;background:#1e1e24}
|
||||||
|
canvas{display:block;width:100vw;height:100vh;outline:none}
|
||||||
|
#overlay{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1e1e24;z-index:10;transition:opacity .4s}
|
||||||
|
#overlay.hidden{opacity:0;pointer-events:none}
|
||||||
|
#overlay .bar-track{width:min(280px,60vw);height:3px;background:#2a2a32;border-radius:2px;margin-top:18px}
|
||||||
|
#overlay .bar-fill{height:100%;width:0%;background:#7c7cff;border-radius:2px;transition:width .15s}
|
||||||
|
#overlay .status{color:#888;font:13px/1 -apple-system,system-ui,sans-serif;margin-top:10px;letter-spacing:.02em}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="overlay">
|
||||||
|
<div class="bar-track"><div class="bar-fill" id="bar"></div></div>
|
||||||
|
<div class="status" id="status">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
|
||||||
|
<script>
|
||||||
|
var bar=document.getElementById('bar');
|
||||||
|
var status=document.getElementById('status');
|
||||||
|
var overlay=document.getElementById('overlay');
|
||||||
|
var Module={
|
||||||
|
canvas:document.getElementById('canvas'),
|
||||||
|
print:function(){console.log(Array.prototype.slice.call(arguments).join(' '))},
|
||||||
|
printErr:function(){console.warn(Array.prototype.slice.call(arguments).join(' '))},
|
||||||
|
setStatus:function(t){
|
||||||
|
if(!t){overlay.classList.add('hidden');return}
|
||||||
|
var m=t.match(/\((\d+(?:\.\d+)?)\/(\d+)\)/);
|
||||||
|
if(m){bar.style.width=(parseInt(m[1])/parseInt(m[2])*100)+'%';status.textContent='Loading\u2026'}
|
||||||
|
else{status.textContent=t}
|
||||||
|
},
|
||||||
|
totalDependencies:0,
|
||||||
|
monitorRunDependencies:function(left){
|
||||||
|
this.totalDependencies=Math.max(this.totalDependencies,left);
|
||||||
|
this.setStatus(left?'Loading... ('+( this.totalDependencies-left)+'/'+this.totalDependencies+')':'');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Module.setStatus('Loading\u2026');
|
||||||
|
window.onerror=function(){Module.setStatus('Error — see console');};
|
||||||
|
</script>
|
||||||
|
<script>Module.locateFile=function(p){return p+'?v=1b9247a1'}</script>
|
||||||
|
<script async type="text/javascript" src="wasm_check2.js?v=1b9247a1"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1765
wasm_check2.js
Normal file
1765
wasm_check2.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
wasm_check2.wasm
Executable file
BIN
wasm_check2.wasm
Executable file
Binary file not shown.
Reference in New Issue
Block a user