iOS lock step keyboard + metal

This commit is contained in:
agra
2026-05-18 17:40:10 +03:00
parent b43472e6ab
commit f9ecf9d00e
68 changed files with 4794 additions and 203 deletions

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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 };
}
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View File

@@ -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; }

View File

@@ -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
View 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; }

View 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 };
}
}

View File

@@ -0,0 +1,2 @@
// Shared type for issue-0033 — Wrap struct.
Wrap :: struct { v: s64 = 0; }

View 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
View 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(); }

View 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 };
}
}

View 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 };
}
}

View File

@@ -0,0 +1,2 @@
// Shared type for issue-0034.
Wrap :: struct { v: s64 = 0; }

14
examples/issue-0034.sx Normal file
View 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;
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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;

View 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,
};
}
}

View File

@@ -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;
} }

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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`)
}; };

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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.

View File

@@ -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,
} }); } });
} }

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
direct a=0 b=1
proto f = 1
proto f = 0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
cl f = 1
cl f = 0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
got pointer: true

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
ok

View File

@@ -0,0 +1 @@
0

View File

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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
w.v = 30

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
stdin extern global non-null: true

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
noop block ran

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
x + y = 142

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
inline block, x = 7

View File

@@ -0,0 +1 @@
1

View 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

View File

@@ -0,0 +1 @@
1

View 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

View File

@@ -0,0 +1 @@
1

View 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
View 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&hellip;</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

File diff suppressed because it is too large Load Diff

BIN
wasm_check.wasm Executable file

Binary file not shown.

50
wasm_check2.html Normal file
View 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&hellip;</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

File diff suppressed because it is too large Load Diff

BIN
wasm_check2.wasm Executable file

Binary file not shown.