From a938c4f900a182964d7db8964ed138415adeb422 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 17 May 2026 19:36:37 +0300 Subject: [PATCH] metal: GPU protocol + MetalGPU renders MSL triangle on iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 8 step 3a of the Metal renderer port: - New library/modules/gpu/ with types.sx (handles + ClearColor + TextureFormat enum), api.sx (GPU :: protocol { ... } covering the lifecycle / per-frame / resource / per-draw surface), and metal.sx (MetalGPU backend implementing the protocol against CAMetalLayer). Resource handles are 1-based indices into backend List(*void) tables. MTL aggregates >16 bytes (MTLRegion, MTLScissorRect) pass via *T to match arm64 Apple's indirect-by-reference ABI; MTLClearColor + CGSize go through the HFA path as direct fn-pointer casts on objc_msgSend. - UIKitPlatform got a gpu_mode: GpuMode toggle + sibling SxMetalView class registration. In metal mode init skips EAGL context, the did_finish_launching IMP skips the EAGL drawable-properties dict, layoutSubviews reads the layer's bounds * dpi_scale into pixel_w/h instead of allocating a GL renderbuffer, and end_frame is a no-op (the MetalGPU owns its own present). - examples/63-metal-clear.sx verifies the pipeline end-to-end on iOS sim — compiles a pass-through MSL shader (packed_float2/packed_float4 to avoid alignment padding), uploads 3 vertices, draws a colored triangle on a dark-blue clear. Compiler fixes (filed-and-fixed in this branch): - inline if X { return E; } followed by a fall-through final expression no longer emits two terminators into the same basic block. Verified by examples/83-inline-if-return-fallthrough.sx. - Top-level type alias Name :: u32; now resolves correctly as the type annotation on a global variable (was treated as ptr {}, breaking comparisons + initializers). Verified by examples/84-global-type-alias.sx. Issue->feature promotion: - 16 historical examples/issue-NNNN.sx repros now confirmed-fixed and renamed to focused feature names (67-82). Each gains a tests/expected/*.txt + .exit pair so the regression suite covers them. - 5 stale issue repros deleted (subsumed by broader tests). Regression suite: 68 passing, 0 failed. macOS chess builds + runs; wasm chess builds; iOS sim GLES chess still renders the full board; iOS sim Metal demo renders the triangle. --- examples/63-metal-clear.sx | 120 ++++ .../{issue-0002.sx => 67-impl-for-builtin.sx} | 7 +- ...3.sx => 68-generic-protocol-constraint.sx} | 11 +- ...{issue-0004.sx => 69-optional-all-null.sx} | 10 +- ...issue-0005.sx => 70-optional-roundtrip.sx} | 7 +- examples/71-int-cmp-in-float-ternary.sx | 16 + ...07.sx => 72-protocol-in-wrapper-struct.sx} | 13 +- ...ue-0008.sx => 73-protocol-list-from-fn.sx} | 16 +- ....sx => 74-protocol-dispatch-via-fn-arg.sx} | 4 +- examples/75-push-context-with-arena.sx | 18 + ...10.sx => 76-closure-returning-protocol.sx} | 10 +- ...-0011.sx => 77-list-items-assign-big-T.sx} | 14 +- ...e-0013.sx => 78-global-compound-assign.sx} | 8 +- ...{issue-0015.sx => 79-global-array-init.sx} | 9 +- ....sx => 80-dot-shorthand-protocol-field.sx} | 16 +- examples/81-global-struct-defaults.sx | 26 + examples/82-xx-target-in-field-assign.sx | 42 ++ examples/83-inline-if-return-fallthrough.sx | 16 + examples/84-global-type-alias.sx | 16 + examples/issue-0006.sx | 21 - examples/issue-0009.sx | 23 - examples/issue-0019.sx | 42 -- examples/issue-0020.sx | 54 -- examples/issue-0021.sx | 81 --- library/modules/gpu/api.sx | 42 ++ library/modules/gpu/metal.sx | 571 ++++++++++++++++++ library/modules/gpu/types.sx | 24 + library/modules/platform/uikit.sx | 178 +++++- src/ir/emit_llvm.zig | 12 +- src/ir/lower.zig | 107 +++- tests/expected/67-impl-for-builtin.exit | 1 + tests/expected/67-impl-for-builtin.txt | 2 + .../68-generic-protocol-constraint.exit | 1 + .../68-generic-protocol-constraint.txt | 3 + tests/expected/69-optional-all-null.exit | 1 + tests/expected/69-optional-all-null.txt | 3 + tests/expected/70-optional-roundtrip.exit | 1 + tests/expected/70-optional-roundtrip.txt | 10 + .../expected/71-int-cmp-in-float-ternary.exit | 1 + .../expected/71-int-cmp-in-float-ternary.txt | 2 + .../72-protocol-in-wrapper-struct.exit | 1 + .../72-protocol-in-wrapper-struct.txt | 4 + tests/expected/73-protocol-list-from-fn.exit | 1 + tests/expected/73-protocol-list-from-fn.txt | 7 + .../74-protocol-dispatch-via-fn-arg.exit | 1 + .../74-protocol-dispatch-via-fn-arg.txt | 8 + .../expected/75-push-context-with-arena.exit | 1 + tests/expected/75-push-context-with-arena.txt | 2 + .../76-closure-returning-protocol.exit | 1 + .../76-closure-returning-protocol.txt | 2 + .../expected/77-list-items-assign-big-T.exit | 1 + tests/expected/77-list-items-assign-big-T.txt | 4 + tests/expected/78-global-compound-assign.exit | 1 + tests/expected/78-global-compound-assign.txt | 10 + tests/expected/79-global-array-init.exit | 1 + tests/expected/79-global-array-init.txt | 2 + .../80-dot-shorthand-protocol-field.exit | 1 + .../80-dot-shorthand-protocol-field.txt | 3 + tests/expected/81-global-struct-defaults.exit | 1 + tests/expected/81-global-struct-defaults.txt | 6 + .../82-xx-target-in-field-assign.exit | 1 + .../expected/82-xx-target-in-field-assign.txt | 2 + .../83-inline-if-return-fallthrough.exit | 1 + .../83-inline-if-return-fallthrough.txt | 1 + tests/expected/84-global-type-alias.exit | 1 + tests/expected/84-global-type-alias.txt | 1 + 66 files changed, 1248 insertions(+), 376 deletions(-) create mode 100644 examples/63-metal-clear.sx rename examples/{issue-0002.sx => 67-impl-for-builtin.sx} (70%) rename examples/{issue-0003.sx => 68-generic-protocol-constraint.sx} (78%) rename examples/{issue-0004.sx => 69-optional-all-null.sx} (71%) rename examples/{issue-0005.sx => 70-optional-roundtrip.sx} (83%) create mode 100644 examples/71-int-cmp-in-float-ternary.sx rename examples/{issue-0007.sx => 72-protocol-in-wrapper-struct.sx} (68%) rename examples/{issue-0008.sx => 73-protocol-list-from-fn.sx} (63%) rename examples/{issue-0008-bare.sx => 74-protocol-dispatch-via-fn-arg.sx} (81%) create mode 100644 examples/75-push-context-with-arena.sx rename examples/{issue-0010.sx => 76-closure-returning-protocol.sx} (56%) rename examples/{issue-0011.sx => 77-list-items-assign-big-T.sx} (64%) rename examples/{issue-0013.sx => 78-global-compound-assign.sx} (68%) rename examples/{issue-0015.sx => 79-global-array-init.sx} (56%) rename examples/{issue-0018.sx => 80-dot-shorthand-protocol-field.sx} (75%) create mode 100644 examples/81-global-struct-defaults.sx create mode 100644 examples/82-xx-target-in-field-assign.sx create mode 100644 examples/83-inline-if-return-fallthrough.sx create mode 100644 examples/84-global-type-alias.sx delete mode 100644 examples/issue-0006.sx delete mode 100644 examples/issue-0009.sx delete mode 100644 examples/issue-0019.sx delete mode 100644 examples/issue-0020.sx delete mode 100644 examples/issue-0021.sx create mode 100644 library/modules/gpu/api.sx create mode 100644 library/modules/gpu/metal.sx create mode 100644 library/modules/gpu/types.sx create mode 100644 tests/expected/67-impl-for-builtin.exit create mode 100644 tests/expected/67-impl-for-builtin.txt create mode 100644 tests/expected/68-generic-protocol-constraint.exit create mode 100644 tests/expected/68-generic-protocol-constraint.txt create mode 100644 tests/expected/69-optional-all-null.exit create mode 100644 tests/expected/69-optional-all-null.txt create mode 100644 tests/expected/70-optional-roundtrip.exit create mode 100644 tests/expected/70-optional-roundtrip.txt create mode 100644 tests/expected/71-int-cmp-in-float-ternary.exit create mode 100644 tests/expected/71-int-cmp-in-float-ternary.txt create mode 100644 tests/expected/72-protocol-in-wrapper-struct.exit create mode 100644 tests/expected/72-protocol-in-wrapper-struct.txt create mode 100644 tests/expected/73-protocol-list-from-fn.exit create mode 100644 tests/expected/73-protocol-list-from-fn.txt create mode 100644 tests/expected/74-protocol-dispatch-via-fn-arg.exit create mode 100644 tests/expected/74-protocol-dispatch-via-fn-arg.txt create mode 100644 tests/expected/75-push-context-with-arena.exit create mode 100644 tests/expected/75-push-context-with-arena.txt create mode 100644 tests/expected/76-closure-returning-protocol.exit create mode 100644 tests/expected/76-closure-returning-protocol.txt create mode 100644 tests/expected/77-list-items-assign-big-T.exit create mode 100644 tests/expected/77-list-items-assign-big-T.txt create mode 100644 tests/expected/78-global-compound-assign.exit create mode 100644 tests/expected/78-global-compound-assign.txt create mode 100644 tests/expected/79-global-array-init.exit create mode 100644 tests/expected/79-global-array-init.txt create mode 100644 tests/expected/80-dot-shorthand-protocol-field.exit create mode 100644 tests/expected/80-dot-shorthand-protocol-field.txt create mode 100644 tests/expected/81-global-struct-defaults.exit create mode 100644 tests/expected/81-global-struct-defaults.txt create mode 100644 tests/expected/82-xx-target-in-field-assign.exit create mode 100644 tests/expected/82-xx-target-in-field-assign.txt create mode 100644 tests/expected/83-inline-if-return-fallthrough.exit create mode 100644 tests/expected/83-inline-if-return-fallthrough.txt create mode 100644 tests/expected/84-global-type-alias.exit create mode 100644 tests/expected/84-global-type-alias.txt diff --git a/examples/63-metal-clear.sx b/examples/63-metal-clear.sx new file mode 100644 index 0000000..cc8266f --- /dev/null +++ b/examples/63-metal-clear.sx @@ -0,0 +1,120 @@ +// iOS-only: bring up UIKitPlatform in Metal mode, clear the screen dark +// blue each frame, then draw a colored triangle via the GPU protocol — +// exercises create_shader (MSL compile + pipeline state), create_buffer +// + update_buffer, set_shader, set_vertex_buffer, and draw_triangles. +// Step 3b will port the UI renderer to use this same surface. +// +// Build for iOS sim: +// /Users/agra/projects/sx/zig-out/bin/sx build --target ios-sim \ +// examples/63-metal-clear.sx \ +// -o /tmp/MetalClear --bundle /tmp/MetalClear.app \ +// --bundle-id co.swipelab.metalclear -F ~/Library/Frameworks +// codesign --force --sign - --timestamp=none /tmp/MetalClear.app +// xcrun simctl install booted /tmp/MetalClear.app +// xcrun simctl launch --terminate-running-process booted co.swipelab.metalclear +// +// This file is iOS-only and not part of the JIT regression suite (no +// tests/expected/63-metal-clear.txt). The test runner skips it on macOS. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; +#import "modules/platform/api.sx"; +#import "modules/platform/uikit.sx"; +#import "modules/gpu/api.sx"; +#import "modules/gpu/metal.sx"; + +#framework "UIKit"; +#framework "QuartzCore"; +#framework "OpenGLES"; + +// Pass-through vertex + fragment shader for a colored triangle. Vertex +// layout is { packed_float2 pos; packed_float4 color; } = 24 bytes — +// `packed_*` types have 4-byte alignment so the struct doesn't get +// padded between fields (a plain `float4` would force 16-byte alignment +// and pad the struct out to 32 bytes per vertex). Entry-point names +// (vmain / fmain) match what MetalGPU.create_shader looks up. +TRI_MSL :: #string MSL +#include +using namespace metal; + +struct Vertex { + packed_float2 pos; + packed_float4 color; +}; + +struct RasterizerData { + float4 position [[position]]; + float4 color; +}; + +vertex RasterizerData vmain(uint vid [[vertex_id]], + constant Vertex* vertices [[buffer(0)]]) { + RasterizerData out; + out.position = float4(vertices[vid].pos, 0.0, 1.0); + out.color = float4(vertices[vid].color); + return out; +} + +fragment float4 fmain(RasterizerData in [[stage_in]]) { + return in.color; +} +MSL; + +TRI_VERTS : [18]f32 = .[ + -0.6, -0.4, 1.0, 0.0, 0.0, 1.0, + 0.6, -0.4, 0.0, 1.0, 0.0, 1.0, + 0.0, 0.6, 0.0, 0.0, 1.0, 1.0, +]; + +g_plat : *UIKitPlatform = null; +g_gpu : *MetalGPU = null; +g_shader : ShaderHandle = 0; +g_vbuf : BufferHandle = 0; + +frame :: () { + if g_plat == null { return; } + if g_gpu == null { return; } + + // Lazy-init the GPU on the first frame where the layer is available + // (the layer is created during -[SxAppDelegate didFinishLaunching:] + // which fires AFTER our main() returns into UIApplicationMain). + if g_gpu.layer == null { + if g_plat.gl_layer == null { return; } + if !g_gpu.init(g_plat.gl_layer, g_plat.pixel_w, g_plat.pixel_h) { return; } + } + + // Compile shader + upload vertex buffer once. + if g_shader == 0 { + g_shader = g_gpu.create_shader(TRI_MSL, ""); + if g_shader == 0 { return; } + } + if g_vbuf == 0 { + g_vbuf = g_gpu.create_buffer(72); // 3 verts × 24 bytes + if g_vbuf == 0 { return; } + g_gpu.update_buffer(g_vbuf, xx @TRI_VERTS, 72); + } + + bg : ClearColor = .{ r = 0.07, g = 0.10, b = 0.18, a = 1.0 }; + if !g_gpu.begin_frame(bg) { return; } + g_gpu.set_shader(g_shader); + g_gpu.set_vertex_buffer(g_vbuf); + g_gpu.draw_triangles(0, 3); + g_gpu.end_frame(0.0); +} + +main :: () -> s32 { + inline if OS != .ios { return 0; } + + plat : *UIKitPlatform = xx context.allocator.alloc(size_of(UIKitPlatform)); + plat.gpu_mode = .metal; + if !plat.init("Metal Clear", 0, 0) { return 1; } + g_plat = plat; + + gpu : *MetalGPU = xx context.allocator.alloc(size_of(MetalGPU)); + g_gpu = gpu; + + plat.run_frame_loop(closure(frame)); + plat.shutdown(); + 0; +} diff --git a/examples/issue-0002.sx b/examples/67-impl-for-builtin.sx similarity index 70% rename from examples/issue-0002.sx rename to examples/67-impl-for-builtin.sx index fa38e9e..d390cfb 100644 --- a/examples/issue-0002.sx +++ b/examples/67-impl-for-builtin.sx @@ -1,7 +1,6 @@ -// issue-0002: impl for built-in types fails with "expected type name after 'for'" -// -// `impl Protocol for f32` should work the same as `impl Protocol for MyStruct`. -// Currently the parser rejects built-in type names (f32, s64, bool, etc.) after `for`. +// impl Protocol for built-in scalar types (f32, s64, bool, u32, ...) — +// both static dispatch (`f32.lerp(...)`) and protocol-boxed dispatch via +// `#inline` erasure. Lerpable :: protocol #inline { lerp :: (b: Self, t: f32) -> Self; diff --git a/examples/issue-0003.sx b/examples/68-generic-protocol-constraint.sx similarity index 78% rename from examples/issue-0003.sx rename to examples/68-generic-protocol-constraint.sx index 430075d..d77d205 100644 --- a/examples/issue-0003.sx +++ b/examples/68-generic-protocol-constraint.sx @@ -1,12 +1,5 @@ -// issue-0003: Generic struct with protocol #inline constraint generates wrong LLVM types -// -// When `Animated($T: Lerpable)` is monomorphized with a struct type like `Size`, -// the LLVM IR generates `{}` (empty type) instead of the actual struct layout -// for the `T` parameter in methods like `set_immediate`, `animate_to`, and `lerp`. -// -// Error: "Call parameter type does not match function signature!" -// call void @Animated__0.set_immediate(ptr ..., { float, float } ...) -// expected {} but got { float, float } +// Generic struct `Animated($T: Lerpable)` monomorphized with a struct type — the +// `#inline` protocol constraint participates in method dispatch via `self.from.lerp(...)`. #import "modules/std.sx"; #import "modules/math"; diff --git a/examples/issue-0004.sx b/examples/69-optional-all-null.sx similarity index 71% rename from examples/issue-0004.sx rename to examples/69-optional-all-null.sx index df6bad2..9e0a6c9 100644 --- a/examples/issue-0004.sx +++ b/examples/69-optional-all-null.sx @@ -1,10 +1,6 @@ -// issue-0004: scalar-to-vector conversion when all optional struct fields are null -// -// When a struct has multiple ?f32 fields and ALL are set to null simultaneously, -// passing that struct to a virtual function call triggers: -// "error: scalar-to-vector conversion failed" -// -// Setting at least one field to a concrete value works fine. +// Struct with multiple `?f32` fields, all set to `null` simultaneously, passed +// into a protocol-dispatched method. Exercises the all-null-payload path through +// the boxed call. #import "modules/std.sx"; diff --git a/examples/issue-0005.sx b/examples/70-optional-roundtrip.sx similarity index 83% rename from examples/issue-0005.sx rename to examples/70-optional-roundtrip.sx index 3c68ebd..08573ac 100644 --- a/examples/issue-0005.sx +++ b/examples/70-optional-roundtrip.sx @@ -1,8 +1,5 @@ -// issue-0005: optional f32 field in struct loses value when earlier optional is null -// -// FIXED: null_literal was double-wrapped as Some in struct literal coercion. -// Root cause: inferExprType(null) returns .void, coerceToType(.void, ?f32) -// tried to wrap the already-null value as Some, corrupting the struct. +// Optional `?f32` fields in struct literals — exhaustively combine null/value +// for both fields, through both direct calls and protocol dispatch. #import "modules/std.sx"; diff --git a/examples/71-int-cmp-in-float-ternary.sx b/examples/71-int-cmp-in-float-ternary.sx new file mode 100644 index 0000000..cd8a28b --- /dev/null +++ b/examples/71-int-cmp-in-float-ternary.sx @@ -0,0 +1,16 @@ +// Integer literal `0` on the RHS of an integer comparison stays integer-typed +// even when the comparison is the condition of an `if-then-else` whose result +// type is `f32`. The comparison must not pick up the outer ternary's type. + +#import "modules/std.sx"; + +main :: () -> void { + x : s64 = 42; + + // OK: comparison in statement context + if x != 0 { out("ok\n"); } + + // BUG: comparison as condition of f32 ternary — `0` inferred as f32 + result : f32 = if x != 0 then 1.0 else 2.0; + print("result = {}\n", result); +} diff --git a/examples/issue-0007.sx b/examples/72-protocol-in-wrapper-struct.sx similarity index 68% rename from examples/issue-0007.sx rename to examples/72-protocol-in-wrapper-struct.sx index e37bb9b..b127b18 100644 --- a/examples/issue-0007.sx +++ b/examples/72-protocol-in-wrapper-struct.sx @@ -1,13 +1,6 @@ -// issue-0007: protocol value stores dangling pointer to stack local -// -// When a concrete value is converted to a protocol value inside a function, -// and the protocol value is stored in a List (via a wrapper struct), the -// protocol value's data pointer points to the stack-local variable rather -// than a heap-allocated copy. After the function returns, the pointer is -// dangling and method dispatch crashes (SIGSEGV/SIGBUS). -// -// Inside the function: dispatch works (stack local still alive) -// After the function returns: dispatch crashes (stack local gone) +// Protocol value as a field of a wrapper struct, constructed from a stack +// local inside a function and appended to a `List`. The payload must be +// heap-copied so dispatch survives the constructing function returning. #import "modules/std.sx"; diff --git a/examples/issue-0008.sx b/examples/73-protocol-list-from-fn.sx similarity index 63% rename from examples/issue-0008.sx rename to examples/73-protocol-list-from-fn.sx index 681de28..c002ba6 100644 --- a/examples/issue-0008.sx +++ b/examples/73-protocol-list-from-fn.sx @@ -1,16 +1,6 @@ -// issue-0008: protocol value created in a function and appended to a list -// still stores a dangling stack pointer (issue-0007 fix incomplete) -// -// When a concrete value is converted to a protocol value inside a function -// (either implicitly via append or explicitly) and stored in a List, the -// protocol data pointer targets the function's stack frame instead of a -// heap-allocated copy. -// -// After the function returns, the first dispatch may succeed (stack not yet -// overwritten), but subsequent dispatches crash because the stack memory has -// been reused by other calls. -// -// STATUS: open — issue-0007 fix only covers some cases +// `List(Protocol)` appended from inside a helper function, dispatched +// repeatedly from `main` after the helper returns. Exercises the heap-copy +// path for both implicit-erasure-on-append and pre-erased protocol values. #import "modules/std.sx"; diff --git a/examples/issue-0008-bare.sx b/examples/74-protocol-dispatch-via-fn-arg.sx similarity index 81% rename from examples/issue-0008-bare.sx rename to examples/74-protocol-dispatch-via-fn-arg.sx index 881bd9a..abd8b2c 100644 --- a/examples/issue-0008-bare.sx +++ b/examples/74-protocol-dispatch-via-fn-arg.sx @@ -1,4 +1,6 @@ -// Minimal: protocol dispatch on List(Protocol) items from a function +// Protocol dispatch on `List(Protocol)` items where the list pointer is +// passed into another function — verifies the boxed payload survives an +// extra call frame between erasure and dispatch. #import "modules/std.sx"; diff --git a/examples/75-push-context-with-arena.sx b/examples/75-push-context-with-arena.sx new file mode 100644 index 0000000..49b1739 --- /dev/null +++ b/examples/75-push-context-with-arena.sx @@ -0,0 +1,18 @@ +// `push ` where Context's first field is an `#inline` protocol +// (`allocator: Allocator`) and the value being pushed is an `Arena` upcast to +// that protocol. Exercises save/restore of the boxed context across the push. + +#import "modules/std.sx"; +#import "modules/allocators.sx"; + +main :: () -> void { + arena : Arena = ---; + arena.create(context.allocator, 4096); + + new_ctx := Context.{ allocator = xx @arena, data = context.data }; + push new_ctx { + ptr := context.allocator.alloc(128); + out("inside push\n"); + } + out("after push\n"); +} diff --git a/examples/issue-0010.sx b/examples/76-closure-returning-protocol.sx similarity index 56% rename from examples/issue-0010.sx rename to examples/76-closure-returning-protocol.sx index 831082a..069e685 100644 --- a/examples/issue-0010.sx +++ b/examples/76-closure-returning-protocol.sx @@ -1,11 +1,5 @@ -// issue-0010: Closure returning a protocol value generates invalid LLVM IR -// -// LLVM verification failed: Called function must be a pointer! -// %icall = call addrspace(64) i64 %load56() -// -// A Closure() -> MyProtocol where MyProtocol is a protocol (not #inline) -// fails at codegen. Calling the function directly works fine; only the -// closure dispatch path is broken. +// Closure whose return type is a (non-`#inline`) protocol value — exercises +// the indirect-call path where the result is a boxed protocol. #import "modules/std.sx"; diff --git a/examples/issue-0011.sx b/examples/77-list-items-assign-big-T.sx similarity index 64% rename from examples/issue-0011.sx rename to examples/77-list-items-assign-big-T.sx index ae1a57e..fe8fa96 100644 --- a/examples/issue-0011.sx +++ b/examples/77-list-items-assign-big-T.sx @@ -1,14 +1,6 @@ -// issue-0011: Assigning to List(T).items corrupts adjacent memory when T is large -// -// Writing `list.items = xx 0` overwrites memory beyond the 8-byte items pointer -// when the List's element type T is larger than 32 bytes. The corruption spills -// into the struct field that follows the List in memory. -// -// Works correctly when size_of(T) <= 32. -// Fails when size_of(T) > 32 (e.g., 40 bytes). -// -// Likely cause: codegen confuses size_of(T) with size_of([*]T) when generating -// the store instruction for the items field. +// Assigning to `list.items` writes exactly the items-pointer field even when +// the element type `T` is larger than the pointer (40-byte BigNode here), so +// a sibling field of the enclosing struct is not corrupted. #import "modules/std.sx"; diff --git a/examples/issue-0013.sx b/examples/78-global-compound-assign.sx similarity index 68% rename from examples/issue-0013.sx rename to examples/78-global-compound-assign.sx index 641c48f..0d20f26 100644 --- a/examples/issue-0013.sx +++ b/examples/78-global-compound-assign.sx @@ -1,9 +1,5 @@ -// issue-0013: += on global variables reads initial value instead of current value -// -// `g_counter += 1` compiles as `store(initial_value + 1)` instead of -// `store(load(g_counter) + 1)`. So it always produces the same result. -// -// Workaround: use `g_counter = g_counter + 1` instead of `g_counter += 1` +// `+=` on a global variable loads the current value (not the initializer) +// before storing — same semantics as the explicit `g = g + 1` form. #import "modules/std.sx"; diff --git a/examples/issue-0015.sx b/examples/79-global-array-init.sx similarity index 56% rename from examples/issue-0015.sx rename to examples/79-global-array-init.sx index 1bcde2b..f81f50d 100644 --- a/examples/issue-0015.sx +++ b/examples/79-global-array-init.sx @@ -1,10 +1,5 @@ -// issue-0015: Global array variables with initializers contain all zeros at runtime. -// -// Expected: VALS[0]=-2, VALS[1]=-1, VALS[2]=42 -// Actual: VALS[0]=0, VALS[1]=0, VALS[2]=0 -// -// Global arrays declared with `: [N]T = .[...]` syntax get zero-initialized -// instead of receiving their specified values. +// Global array declared with `: [N]T = .[...]` keeps its initializer values +// (signed-int element type covers negative-literal handling too). #import "modules/std.sx"; diff --git a/examples/issue-0018.sx b/examples/80-dot-shorthand-protocol-field.sx similarity index 75% rename from examples/issue-0018.sx rename to examples/80-dot-shorthand-protocol-field.sx index 105ccb7..683b98f 100644 --- a/examples/issue-0018.sx +++ b/examples/80-dot-shorthand-protocol-field.sx @@ -1,16 +1,6 @@ -// issue-0018: Dot-shorthand `.{...}` for struct with protocol field causes -// LLVM verification error when used in List(T).append from 2+ different -// struct methods. -// -// Trigger: TWO or more structs each with `List(Container)` calling -// `.append(.{ child = d })` — using dot-shorthand. -// -// Works: Only 1 struct doing it, or using explicit `Container.{ child = d }`. -// Fails: 2+ structs → `Invalid InsertValueInst operands!` -// `%si = insertvalue i64 undef, { ptr, ptr } %load, 0` -// -// Likely a monomorphization issue in `List(T).append` when the dot-shorthand -// type inference is resolved from multiple call sites. +// Dot-shorthand `.{ child = d }` for a struct whose first field is a protocol +// value, used as the argument to `List(Container).append` from two distinct +// container types. Exercises the cross-callsite path of dot-shorthand inference. #import "modules/std.sx"; diff --git a/examples/81-global-struct-defaults.sx b/examples/81-global-struct-defaults.sx new file mode 100644 index 0000000..39eb16a --- /dev/null +++ b/examples/81-global-struct-defaults.sx @@ -0,0 +1,26 @@ +// Global struct initialized with `.{}` (or a partial struct literal) honors +// the struct's per-field defaults, matching the function-local behavior. + +#import "modules/std.sx"; + +Foo :: struct { + running: bool = true; + x: s32 = 42; + name: string = "default"; +} + +g_empty : Foo = .{}; +g_partial : Foo = .{ x = 99 }; +g_override : Foo = .{ running = false }; +g_reorder : Foo = .{ x = 7, running = false, name = "hi" }; +g_positional : Foo = .{ false, 13, "pos" }; + +main :: () -> void { + l_empty : Foo = .{}; + print("local running={} x={} name={}\n", l_empty.running, l_empty.x, l_empty.name); + print("g_empty running={} x={} name={}\n", g_empty.running, g_empty.x, g_empty.name); + print("g_partial running={} x={} name={}\n", g_partial.running, g_partial.x, g_partial.name); + print("g_override running={} x={} name={}\n", g_override.running, g_override.x, g_override.name); + print("g_reorder running={} x={} name={}\n", g_reorder.running, g_reorder.x, g_reorder.name); + print("g_positional running={} x={} name={}\n", g_positional.running, g_positional.x, g_positional.name); +} diff --git a/examples/82-xx-target-in-field-assign.sx b/examples/82-xx-target-in-field-assign.sx new file mode 100644 index 0000000..a3edf3b --- /dev/null +++ b/examples/82-xx-target-in-field-assign.sx @@ -0,0 +1,42 @@ +// `xx` cast inside an RHS expression assigned to a struct field takes its +// target type from the field, not from the enclosing function's return type. +// Covers if-then-else RHS and binary-op RHS variants. + +#import "modules/std.sx"; + +Foo :: struct { + pixel_w: s32; + dpi: f32; + last_perf: s64; + delta_time: f32; +} +FC :: struct { a: f32; b: f32; c: s32; d: s32; e: f32; f: f32; } + +// If-then-else RHS in a function whose return type is not f32. +calc_bool :: (self: *Foo, wf: f32) -> bool { + self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; + true; +} + +// Binary-op RHS in a struct-returning function. The xx casts must target f32, +// not the FC return-struct shape. +begin :: (self: *Foo, current: s64, freq: s64) -> FC { + if self.last_perf > 0 { + self.delta_time = xx (current - self.last_perf) / xx freq; + } + FC.{ a = 1.0, b = 2.0, c = 3, d = 4, e = 5.0, f = 6.0 }; +} + +main :: () -> void { + f : *Foo = xx malloc(size_of(Foo)); + f.pixel_w = 2880; + f.dpi = 0.0; + f.last_perf = 1000; + f.delta_time = 0.0; + + _ := calc_bool(f, 1440.0); + print("dpi={}\n", f.dpi); + + fc := begin(f, 1500, 1000); + print("delta={} fc.a={}\n", f.delta_time, fc.a); +} diff --git a/examples/83-inline-if-return-fallthrough.sx b/examples/83-inline-if-return-fallthrough.sx new file mode 100644 index 0000000..55a9232 --- /dev/null +++ b/examples/83-inline-if-return-fallthrough.sx @@ -0,0 +1,16 @@ +// `inline if COND { return E; }` followed by a fall-through final expression: +// when COND evaluates true at comptime, the body is spliced unconditionally +// and the trailing expression must NOT also be emitted into the same LLVM +// block (only one terminator per block). + +#import "modules/std.sx"; +#import "modules/compiler.sx"; + +do_it :: () -> bool { + inline if OS != .ios { return false; } + true; +} + +main :: () -> s32 { + if do_it() then 0 else 1; +} diff --git a/examples/84-global-type-alias.sx b/examples/84-global-type-alias.sx new file mode 100644 index 0000000..e1e7f08 --- /dev/null +++ b/examples/84-global-type-alias.sx @@ -0,0 +1,16 @@ +// Top-level type alias `Handle :: u32;` resolves to its target type in every +// position — function signatures, type annotations on globals, and initializer +// literal coercion. + +#import "modules/std.sx"; + +Handle :: u32; + +ok :: () -> Handle { 0; } + +g : Handle = 0; + +main :: () -> s32 { + g = ok(); + if g == 0 then 0 else 1; +} diff --git a/examples/issue-0006.sx b/examples/issue-0006.sx deleted file mode 100644 index 426184b..0000000 --- a/examples/issue-0006.sx +++ /dev/null @@ -1,21 +0,0 @@ -// issue-0006: literal `0` in integer comparison inferred as float inside f32 ternary -// -// When `s64 != 0` is used as the condition of a ternary whose result type is f32, -// the literal `0` in the comparison leaks the ternary's f32 type instead of matching -// the LHS s64 type. This generates invalid LLVM IR: -// %icmp = icmp ne i64 %load, float 0.000000e+00 -// -// The same comparison works fine in a regular if-statement. - -#import "modules/std.sx"; - -main :: () -> void { - x : s64 = 42; - - // OK: comparison in statement context - if x != 0 { out("ok\n"); } - - // BUG: comparison as condition of f32 ternary — `0` inferred as f32 - result : f32 = if x != 0 then 1.0 else 2.0; - print("result = {}\n", result); -} diff --git a/examples/issue-0009.sx b/examples/issue-0009.sx deleted file mode 100644 index f46a541..0000000 --- a/examples/issue-0009.sx +++ /dev/null @@ -1,23 +0,0 @@ -// issue-0009: `push` with Context containing inline protocol triggers LLVM verification error -// -// LLVM verification failed: Invalid InsertValueInst operands! -// %si24 = insertvalue { ptr, ptr, ptr } undef, { ptr, ptr, ptr } %si21, 0 -// -// Context contains `allocator: Allocator` where `Allocator :: protocol #inline`. -// push saves/restores context as a value, but LLVM lowering mishandles the struct -// when the first field is an inline protocol cast from a different impl (Arena). - -#import "modules/std.sx"; -#import "modules/allocators.sx"; - -main :: () -> void { - arena : Arena = ---; - arena.create(context.allocator, 4096); - - new_ctx := Context.{ allocator = xx @arena, data = context.data }; - push new_ctx { - ptr := context.allocator.alloc(128); - out("inside push\n"); - } - out("after push\n"); -} diff --git a/examples/issue-0019.sx b/examples/issue-0019.sx deleted file mode 100644 index 2308ebf..0000000 --- a/examples/issue-0019.sx +++ /dev/null @@ -1,42 +0,0 @@ -// issue-0019: #import c symbols are globally visible instead of scoped to the importing file -// -// When a file uses `#import c { #include "foo.h"; #source "foo.c"; }`, -// the C symbols become available to ALL files in the compilation unit, -// not just the file that imported them. -// -// This means a file can call C functions without importing the module -// that declares them, as long as some other file in the project does. -// -// Expected: C symbols from `#import c` should only be visible in files -// that directly (or transitively via SX #import) import the module. -// -// Repro: -// - a.sx: `#import c { #include "some_lib.h"; #source "some_lib.c"; };` -// - b.sx: does NOT import a.sx, but calls some_lib_function() — compiles successfully -// -// In the game project: -// - main.sx imports modules/stb_truetype.sx (which has #import c for kbts/stbtt) -// - ui/glyph_cache.sx does NOT import modules/stb_truetype.sx -// - ui/glyph_cache.sx calls kbts_ShapeRun, stbtt_InitFont, etc. — compiles fine -// - If main.sx removed the import, glyph_cache.sx would break - -// Minimal repro structure (two files): - -// --- module_with_c.sx --- -// #import c { -// #include "vendors/some_lib.h"; -// #source "vendors/some_lib.c"; -// }; -// -// uses_c :: () -> s32 { -// some_lib_function(); -// } - -// --- main.sx (this file) --- -// #import "module_with_c.sx"; -// -// main :: () -> s32 { -// // This should fail because we never imported the C module directly, -// // but currently it compiles: -// some_lib_function(); -// } diff --git a/examples/issue-0020.sx b/examples/issue-0020.sx deleted file mode 100644 index 4dc24fc..0000000 --- a/examples/issue-0020.sx +++ /dev/null @@ -1,54 +0,0 @@ -// issue-0020: Global `Foo = .{}` zero-initializes, ignoring field defaults -// -// Struct field defaults declared via `field: T = expr;` are honored when the -// struct is constructed at function-local scope, but are silently dropped -// when the struct is declared at module scope with `= .{}`. -// -// Repro: -// -// Foo :: struct { -// running: bool = true; // default -// } -// -// g_foo : Foo = .{}; // global → g_foo.running == false (BUG) -// -// main :: () { -// l_foo : Foo = .{}; // local → l_foo.running == true (correct) -// } -// -// Surface bites: -// -// - In the SX Chess game, the SDL3 platform backend stores a `running: bool -// = true` field on `SdlPlatform`. With `g_plat : SdlPlatform = .{};` the -// main loop's `while self.running { ... }` exits immediately because -// `running` was zero-initialized despite the field default. -// - Workaround: assign defaults explicitly in the type's `init` method, or -// spell every field out at the global construction site: -// g_plat : SdlPlatform = .{ running = true }; -// -// Likely cause: the globals path emits an LLVM ConstantAggregateZero (or -// memset-to-zero) for the initializer, skipping the per-field default-expr -// lowering used for local declarations. -// -// This file is a runnable repro: locals print "running=true", globals print -// "running=false". - -#import "modules/std.sx"; - -Foo :: struct { - running: bool = true; - x: s32 = 42; -} - -g_foo : Foo = .{}; - -main :: () -> void { - out("global running="); - out(if g_foo.running then "true" else "false"); - out("\n"); - - l_foo : Foo = .{}; - out("local running="); - out(if l_foo.running then "true" else "false"); - out("\n"); -} diff --git a/examples/issue-0021.sx b/examples/issue-0021.sx deleted file mode 100644 index 0c0aca2..0000000 --- a/examples/issue-0021.sx +++ /dev/null @@ -1,81 +0,0 @@ -// issue-0021: enclosing function's return type bleeds into `xx`'s target -// type inside an `if-then-else` expression on the RHS of a struct-field -// assignment -// -// ── Repro ────────────────────────────────────────────────────────────────── -// -// Foo :: struct { pixel_w: s32; dpi: f32; } -// -// calc_void :: (self: *Foo, wf: f32) { -// self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; -// } -// -// calc_bool :: (self: *Foo, wf: f32) -> bool { -// self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; -// true; -// } -// -// With `f.pixel_w = 2880` and `wf = 1440.0`: -// - `calc_void` produces `f.dpi = 2.0` (correct). -// - `calc_bool` produces `f.dpi = 0.0` (wrong). -// -// Only difference: the enclosing function's declared return type. The `xx` -// cast appears to take its target type from the enclosing function's return -// type rather than from the assignment LHS (which is `f32`). When the return -// type is `bool`, the divide is lowered against a non-numeric/zero-valued -// operand. -// -// ── Related, possibly same root cause ─────────────────────────────────────── -// -// In a struct-returning function, this form makes LLVM verification fail with -// `udiv { float, float, i32, i32, float, float }` — the divide is lowered as -// integer division over the function's return-struct shape: -// -// FC :: struct { a: f32; b: f32; c: s32; d: s32; e: f32; f: f32; } -// begin :: (self: *Foo) -> FC { -// if self.last_perf > 0 { -// self.delta_time = xx (current - self.last_perf) / xx freq; -// } -// FC.{ ... }; -// } -// -// ── Workaround ───────────────────────────────────────────────────────────── -// -// Hoist the `xx` cast into its own variable so the divide sees two -// already-typed f32 values: -// -// pw : f32 = xx self.pixel_w; -// self.dpi = if wf > 0.0 then pw / wf else 1.0; -// -// ── Real-world impact ────────────────────────────────────────────────────── -// -// The SX Chess game's dpi_scale calculation took this form inside -// `SdlPlatform.init` (which returns `bool`). On a retina display the -// dpi_scale silently became 0, so the glyph cache rasterized at scale=0 and -// every text label rendered invisibly. - -#import "modules/std.sx"; - -Foo :: struct { pixel_w: s32; dpi: f32; } - -calc_void :: (self: *Foo, wf: f32) { - self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; -} - -calc_bool :: (self: *Foo, wf: f32) -> bool { - self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; - true; -} - -main :: () -> void { - f : *Foo = xx malloc(size_of(Foo)); - f.pixel_w = 2880; - - f.dpi = 0.0; - calc_void(f, 1440.0); - out("void-return (expect 200): "); out(int_to_string(xx (f.dpi * 100.0))); out("\n"); - - f.dpi = 0.0; - calc_bool(f, 1440.0); - out("bool-return (expect 200): "); out(int_to_string(xx (f.dpi * 100.0))); out("\n"); -} diff --git a/library/modules/gpu/api.sx b/library/modules/gpu/api.sx new file mode 100644 index 0000000..ce7d82e --- /dev/null +++ b/library/modules/gpu/api.sx @@ -0,0 +1,42 @@ +#import "modules/std.sx"; +#import "modules/gpu/types.sx"; + +// GPU is the rendering-API abstraction. Concrete backends live as siblings +// of this file: `metal.sx` (iOS, eventually macOS), `vulkan.sx` (Linux/ +// Android, plus macOS via MoltenVK), `webgpu.sx` (wasm). The SDL-backed +// GL renderer used by the desktop+wasm path stays as-is until those +// backends land. + +GPU :: protocol { + // Bind the GPU to a backend-specific render target (e.g. a + // CAMetalLayer on iOS). pixel_w/pixel_h are the drawable's pixel + // dimensions; call resize when they change. + init :: (target: *void, pixel_w: s32, pixel_h: s32) -> bool; + shutdown :: (); + + resize :: (pixel_w: s32, pixel_h: s32); + + begin_frame :: (clear: ClearColor) -> bool; + + // target_time is the host clock time at which the drawable should be + // presented (units match the platform's CADisplayLink.targetTimestamp + // on Apple). Metal forwards it to presentDrawable:atTime: to cap the + // pipeline at one frame so the inset slide lands on the same vsync as + // UIKit's keyboard view. GL backends ignore it. + end_frame :: (target_time: f64); + + create_shader :: (vsrc: string, fsrc: string) -> ShaderHandle; + create_buffer :: (size_bytes: s64) -> BufferHandle; + update_buffer :: (buf: BufferHandle, data: *void, size_bytes: s64); + create_texture :: (w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle; + update_texture_region :: (tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void); + + set_shader :: (sh: ShaderHandle); + set_vertex_buffer :: (buf: BufferHandle); + set_texture :: (slot: u32, tex: TextureHandle); + set_vertex_constants :: (slot: u32, data: *void, size_bytes: s64); + set_scissor :: (x: s32, y: s32, w: s32, h: s32); + disable_scissor :: (); + + draw_triangles :: (vertex_offset: s32, vertex_count: s32); +} diff --git a/library/modules/gpu/metal.sx b/library/modules/gpu/metal.sx new file mode 100644 index 0000000..99aa4cf --- /dev/null +++ b/library/modules/gpu/metal.sx @@ -0,0 +1,571 @@ +// Metal backend for the GPU protocol. iOS-only for now; macOS later. +// +// Linking is per-target via the consumer's build.sx: +// opts.add_framework("Metal") +// opts.add_framework("QuartzCore") // CAMetalLayer lives here +// `#framework "Metal"` below adds it to iOS-target link lines automatically; +// non-iOS targets don't reach the Metal-touching code paths. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; +#import "modules/gpu/types.sx"; +#import "modules/gpu/api.sx"; + +#framework "Metal"; + +// MTLCreateSystemDefaultDevice lives in the Metal framework as a plain C +// function. Returns id retained +1; we leak it for now since +// the device lives for the whole process. +MTLCreateSystemDefaultDevice :: () -> *void #foreign; + +// Pixel formats. +MTL_PIXEL_FORMAT_BGRA8_UNORM :u64: 80; +MTL_PIXEL_FORMAT_RGBA8_UNORM :u64: 70; +MTL_PIXEL_FORMAT_R8_UNORM :u64: 10; + +// MTLLoadAction / MTLStoreAction. +MTL_LOAD_ACTION_CLEAR :u64: 2; +MTL_STORE_ACTION_STORE :u64: 1; + +// MTLPrimitiveType. +MTL_PRIMITIVE_TYPE_TRIANGLE :u64: 3; + +// MTLBlendFactor — the subset used for normal alpha blending. +MTL_BLEND_FACTOR_SRC_ALPHA :u64: 4; +MTL_BLEND_FACTOR_ONE_MINUS_SRC_A :u64: 5; + +// CGSize is a 2-element f64 HFA. arm64 Apple ABI puts it in d0,d1 — direct +// fn-pointer cast on objc_msgSend with a CGSize arg does the right thing. +CGSize :: struct { width: f64; height: f64; } + +// MTLClearColor is a 4-element f64 HFA. Same ABI story — passes in d0..d3. +MTLClearColor :: struct { + red: f64; + green: f64; + blue: f64; + alpha: f64; +} + +// MTLOrigin / MTLSize / MTLRegion / MTLScissorRect — integer aggregates. +// MTLRegion is 48 bytes and MTLScissorRect is 32 bytes; arm64 Apple ABI +// passes >16-byte composites by reference (address in the next register). +// We declare the call shapes with `*MTLRegion` etc., construct a local on +// the stack, and pass `@local` — the machine state matches what the Obj-C +// method expects. +MTLOrigin :: struct { x: u64; y: u64; z: u64; } +MTLSize :: struct { width: u64; height: u64; depth: u64; } +MTLRegion :: struct { origin: MTLOrigin; size: MTLSize; } +MTLScissorRect :: struct { x: u64; y: u64; width: u64; height: u64; } + +// Pixel sub-format storage for textures. Tracks the bytes-per-pixel for the +// upload path (replaceRegion needs bytesPerRow which is bpp × width). +TextureSlot :: struct { + tex: *void = null; + bytes_per_pixel: u32 = 0; +} + +MetalGPU :: struct { + device: *void = null; // id + queue: *void = null; // id + layer: *void = null; // CAMetalLayer* + pixel_w: s32 = 0; + pixel_h: s32 = 0; + + // Per-frame transients. Live only between begin_frame and end_frame. + drawable: *void = null; // id + cmd_buffer: *void = null; // id + encoder: *void = null; // id + + // Resource tables. Handles are 1-based indices (0 = invalid). + shaders: List(*void) = .{}; // MTLRenderPipelineState* + buffers: List(*void) = .{}; // MTLBuffer* + textures: List(TextureSlot) = .{}; +} + +impl GPU for MetalGPU { + init :: (self: *MetalGPU, target: *void, pixel_w: s32, pixel_h: s32) -> bool { + inline if OS != .ios { return false; } + self.layer = target; + self.pixel_w = pixel_w; + self.pixel_h = pixel_h; + metal_init_ios(self); + } + + shutdown :: (self: *MetalGPU) { + // Metal objects clean up at process exit on iOS. A real shutdown + // would send `release` to queue + device. + } + + resize :: (self: *MetalGPU, pixel_w: s32, pixel_h: s32) { + self.pixel_w = pixel_w; + self.pixel_h = pixel_h; + inline if OS == .ios { + metal_resize_ios(self); + } + } + + begin_frame :: (self: *MetalGPU, clear: ClearColor) -> bool { + inline if OS != .ios { return false; } + metal_begin_frame_ios(self, clear); + } + + end_frame :: (self: *MetalGPU, target_time: f64) { + inline if OS == .ios { + metal_end_frame_ios(self, target_time); + } + } + + // ── Resources ──────────────────────────────────────────────────────── + // Handle = 1-based index into the backing List (0 = invalid). The bulk + // of each method lives in an iOS-only helper for readability — the impl + // method just guards non-iOS and delegates. + + create_shader :: (self: *MetalGPU, vsrc: string, fsrc: string) -> ShaderHandle { + inline if OS != .ios { return 0; } + metal_create_shader_ios(self, vsrc); + } + + create_buffer :: (self: *MetalGPU, size_bytes: s64) -> BufferHandle { + inline if OS != .ios { return 0; } + metal_create_buffer_ios(self, size_bytes); + } + + update_buffer :: (self: *MetalGPU, buf: BufferHandle, data: *void, size_bytes: s64) { + inline if OS == .ios { + metal_update_buffer_ios(self, buf, data, size_bytes); + } + } + + create_texture :: (self: *MetalGPU, w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle { + inline if OS != .ios { return 0; } + metal_create_texture_ios(self, w, h, format, pixels); + } + + update_texture_region :: (self: *MetalGPU, tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void) { + inline if OS == .ios { + metal_update_texture_region_ios(self, tex, x, y, w, h, pixels); + } + } + + // ── Per-draw state ─────────────────────────────────────────────────── + // All operate on `self.encoder`, which is live only between begin_frame + // and end_frame. Calling these outside that window is a silent no-op. + + set_shader :: (self: *MetalGPU, sh: ShaderHandle) { + inline if OS == .ios { + metal_set_shader_ios(self, sh); + } + } + + set_vertex_buffer :: (self: *MetalGPU, buf: BufferHandle) { + inline if OS == .ios { + metal_set_vertex_buffer_ios(self, buf); + } + } + + set_texture :: (self: *MetalGPU, slot: u32, tex: TextureHandle) { + inline if OS == .ios { + metal_set_texture_ios(self, slot, tex); + } + } + + set_vertex_constants :: (self: *MetalGPU, slot: u32, data: *void, size_bytes: s64) { + inline if OS == .ios { + metal_set_vertex_constants_ios(self, slot, data, size_bytes); + } + } + + set_scissor :: (self: *MetalGPU, x: s32, y: s32, w: s32, h: s32) { + inline if OS == .ios { + metal_set_scissor_ios(self, x, y, w, h); + } + } + + disable_scissor :: (self: *MetalGPU) { + inline if OS == .ios { + metal_disable_scissor_ios(self); + } + } + + draw_triangles :: (self: *MetalGPU, vertex_offset: s32, vertex_count: s32) { + inline if OS == .ios { + metal_draw_triangles_ios(self, vertex_offset, vertex_count); + } + } +} + +// ─────────────────────────────────────────────────────────────────────────── +// iOS-only helpers — only reachable from `inline if OS == .ios` call sites, +// so non-iOS builds never reference the unresolved Metal symbols below. +// ─────────────────────────────────────────────────────────────────────────── + +metal_init_ios :: (self: *MetalGPU) -> bool { + inline if OS != .ios { return false; } + if self.layer == null { return false; } + + self.device = MTLCreateSystemDefaultDevice(); + if self.device == null { return false; } + + msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; + msg_ou : (*void, *void, u64) -> void = xx objc_msgSend; + msg_ob : (*void, *void, u8) -> void = xx objc_msgSend; + msg_osize : (*void, *void, CGSize) -> void = xx objc_msgSend; + msg_o : (*void, *void) -> *void = xx objc_msgSend; + + msg_oo(self.layer, sel_registerName("setDevice:".ptr), self.device); + msg_ou(self.layer, sel_registerName("setPixelFormat:".ptr), MTL_PIXEL_FORMAT_BGRA8_UNORM); + msg_ob(self.layer, sel_registerName("setFramebufferOnly:".ptr), 1); + + size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h }; + msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size); + + self.queue = msg_o(self.device, sel_registerName("newCommandQueue".ptr)); + if self.queue == null { return false; } + + true; +} + +metal_resize_ios :: (self: *MetalGPU) { + inline if OS != .ios { return; } + if self.layer == null { return; } + + msg_osize : (*void, *void, CGSize) -> void = xx objc_msgSend; + size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h }; + msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size); +} + +metal_begin_frame_ios :: (self: *MetalGPU, clear: ClearColor) -> bool { + inline if OS != .ios { return false; } + if self.layer == null { return false; } + if self.queue == null { return false; } + + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; + msg_oo_ret : (*void, *void, *void) -> *void = xx objc_msgSend; + msg_ou : (*void, *void, u64) -> void = xx objc_msgSend; + msg_ouret : (*void, *void, u64) -> *void = xx objc_msgSend; + msg_oclear : (*void, *void, MTLClearColor) -> void = xx objc_msgSend; + + // drawable = [layer nextDrawable] + self.drawable = msg_o(self.layer, sel_registerName("nextDrawable".ptr)); + if self.drawable == null { return false; } + + // tex = [drawable texture] + drawable_texture := msg_o(self.drawable, sel_registerName("texture".ptr)); + + // pass = [MTLRenderPassDescriptor renderPassDescriptor] (autoreleased) + MTLRenderPassDescriptor := objc_getClass("MTLRenderPassDescriptor".ptr); + pass := msg_o(MTLRenderPassDescriptor, sel_registerName("renderPassDescriptor".ptr)); + + // color0 = pass.colorAttachments[0] + attachments := msg_o(pass, sel_registerName("colorAttachments".ptr)); + color0 := msg_ouret(attachments, sel_registerName("objectAtIndexedSubscript:".ptr), 0); + + msg_oo(color0, sel_registerName("setTexture:".ptr), drawable_texture); + msg_ou(color0, sel_registerName("setLoadAction:".ptr), MTL_LOAD_ACTION_CLEAR); + msg_ou(color0, sel_registerName("setStoreAction:".ptr), MTL_STORE_ACTION_STORE); + + mtl_clear := MTLClearColor.{ + red = xx clear.r, + green = xx clear.g, + blue = xx clear.b, + alpha = xx clear.a, + }; + msg_oclear(color0, sel_registerName("setClearColor:".ptr), mtl_clear); + + // cmd = [queue commandBuffer] (autoreleased) + self.cmd_buffer = msg_o(self.queue, sel_registerName("commandBuffer".ptr)); + if self.cmd_buffer == null { return false; } + + // encoder = [cmd renderCommandEncoderWithDescriptor:pass] (autoreleased) + self.encoder = msg_oo_ret(self.cmd_buffer, + sel_registerName("renderCommandEncoderWithDescriptor:".ptr), pass); + if self.encoder == null { return false; } + + true; +} + +metal_end_frame_ios :: (self: *MetalGPU, target_time: f64) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + if self.cmd_buffer == null { return; } + if self.drawable == null { return; } + + msg_v : (*void, *void) -> void = xx objc_msgSend; + msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; + msg_ood : (*void, *void, *void, f64) -> void = xx objc_msgSend; + + msg_v(self.encoder, sel_registerName("endEncoding".ptr)); + + // target_time > 0 → presentDrawable:atTime: (lockstep path). + // target_time == 0 → fall back to presentDrawable: (immediate). + if target_time > 0.0 { + msg_ood(self.cmd_buffer, sel_registerName("presentDrawable:atTime:".ptr), + self.drawable, target_time); + } else { + msg_oo(self.cmd_buffer, sel_registerName("presentDrawable:".ptr), self.drawable); + } + + msg_v(self.cmd_buffer, sel_registerName("commit".ptr)); + + self.encoder = null; + self.cmd_buffer = null; + self.drawable = null; +} + +// ── Shader (MSL pipeline state) ────────────────────────────────────────── +// Compile the MSL source, look up the conventional entry points `vmain` +// (vertex) and `fmain` (fragment), and produce an `MTLRenderPipelineState` +// targeted at the layer's BGRA8 surface with standard alpha blending. +// The fsrc parameter is ignored — Metal's library is one MSL file with +// both functions; pass the combined source as vsrc. + +metal_create_shader_ios :: (self: *MetalGPU, src: string) -> u32 { + inline if OS != .ios { return 0; } + if self.device == null { return 0; } + + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; + msg_oo_r : (*void, *void, *void) -> *void = xx objc_msgSend; + msg_ou : (*void, *void, u64) -> void = xx objc_msgSend; + msg_ouret: (*void, *void, u64) -> *void = xx objc_msgSend; + msg_ob : (*void, *void, u8) -> void = xx objc_msgSend; + + // [device newLibraryWithSource:src options:nil error:&err] + msg_lib : (*void, *void, *void, *void, **void) -> *void = xx objc_msgSend; + src_ns := ns_string(src.ptr); + err : *void = null; + library := msg_lib(self.device, + sel_registerName("newLibraryWithSource:options:error:".ptr), + src_ns, xx 0, @err); + if library == null { + NSLog(ns_string("[metal] MSL compile failed\n".ptr)); + return 0; + } + + vfn := msg_oo_r(library, sel_registerName("newFunctionWithName:".ptr), + ns_string("vmain".ptr)); + ffn := msg_oo_r(library, sel_registerName("newFunctionWithName:".ptr), + ns_string("fmain".ptr)); + if vfn == null { NSLog(ns_string("[metal] missing vmain in MSL\n".ptr)); return 0; } + if ffn == null { NSLog(ns_string("[metal] missing fmain in MSL\n".ptr)); return 0; } + + MTLRenderPipelineDescriptor := objc_getClass("MTLRenderPipelineDescriptor".ptr); + desc := msg_o(MTLRenderPipelineDescriptor, sel_registerName("alloc".ptr)); + desc = msg_o(desc, sel_registerName("init".ptr)); + + msg_oo(desc, sel_registerName("setVertexFunction:".ptr), vfn); + msg_oo(desc, sel_registerName("setFragmentFunction:".ptr), ffn); + + // colorAttachments[0]: pixel format + alpha blending. + atts := msg_o(desc, sel_registerName("colorAttachments".ptr)); + att0 := msg_ouret(atts, sel_registerName("objectAtIndexedSubscript:".ptr), 0); + msg_ou(att0, sel_registerName("setPixelFormat:".ptr), MTL_PIXEL_FORMAT_BGRA8_UNORM); + msg_ob(att0, sel_registerName("setBlendingEnabled:".ptr), 1); + msg_ou(att0, sel_registerName("setSourceRGBBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA); + msg_ou(att0, sel_registerName("setDestinationRGBBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A); + msg_ou(att0, sel_registerName("setSourceAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA); + msg_ou(att0, sel_registerName("setDestinationAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A); + + msg_pipe : (*void, *void, *void, **void) -> *void = xx objc_msgSend; + err2 : *void = null; + state := msg_pipe(self.device, + sel_registerName("newRenderPipelineStateWithDescriptor:error:".ptr), + desc, @err2); + if state == null { + NSLog(ns_string("[metal] pipeline state creation failed\n".ptr)); + return 0; + } + + self.shaders.append(state); + xx self.shaders.len; +} + +// ── Buffers ────────────────────────────────────────────────────────────── +// Shared-memory MTLBuffer (CPU + GPU visible on UMA hardware). `contents` +// returns the mapped pointer for memcpy uploads. + +metal_create_buffer_ios :: (self: *MetalGPU, size_bytes: s64) -> u32 { + inline if OS != .ios { return 0; } + if self.device == null { return 0; } + if size_bytes <= 0 { return 0; } + + // MTLResourceStorageModeShared is the default (option value 0). + msg_buf : (*void, *void, u64, u64) -> *void = xx objc_msgSend; + buf := msg_buf(self.device, + sel_registerName("newBufferWithLength:options:".ptr), + xx size_bytes, 0); + if buf == null { return 0; } + + self.buffers.append(buf); + xx self.buffers.len; +} + +metal_update_buffer_ios :: (self: *MetalGPU, handle: u32, data: *void, size_bytes: s64) { + inline if OS != .ios { return; } + buf := metal_lookup_buffer(self, handle); + if buf == null { return; } + if data == null { return; } + if size_bytes <= 0 { return; } + + msg_o : (*void, *void) -> *void = xx objc_msgSend; + dst := msg_o(buf, sel_registerName("contents".ptr)); + if dst == null { return; } + memcpy(dst, data, size_bytes); +} + +metal_lookup_buffer :: (self: *MetalGPU, handle: u32) -> *void { + inline if OS != .ios { return null; } + if handle == 0 { return null; } + h64 : s64 = xx handle; + if h64 > self.buffers.len { return null; } + self.buffers.items[handle - 1]; +} + +metal_lookup_shader :: (self: *MetalGPU, handle: u32) -> *void { + inline if OS != .ios { return null; } + if handle == 0 { return null; } + h64 : s64 = xx handle; + if h64 > self.shaders.len { return null; } + self.shaders.items[handle - 1]; +} + +// ── Textures ───────────────────────────────────────────────────────────── + +metal_create_texture_ios :: (self: *MetalGPU, w: s32, h: s32, format: TextureFormat, pixels: *void) -> u32 { + inline if OS != .ios { return 0; } + if self.device == null { return 0; } + if w <= 0 { return 0; } + if h <= 0 { return 0; } + + pixel_format : u64 = 0; + bytes_per_pixel : u32 = 0; + if format == .rgba8 { + pixel_format = MTL_PIXEL_FORMAT_RGBA8_UNORM; + bytes_per_pixel = 4; + } else { + pixel_format = MTL_PIXEL_FORMAT_R8_UNORM; + bytes_per_pixel = 1; + } + + // [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:width:height:mipmapped:] + MTLTextureDescriptor := objc_getClass("MTLTextureDescriptor".ptr); + msg_desc : (*void, *void, u64, u64, u64, u8) -> *void = xx objc_msgSend; + desc := msg_desc(MTLTextureDescriptor, + sel_registerName("texture2DDescriptorWithPixelFormat:width:height:mipmapped:".ptr), + pixel_format, xx w, xx h, 0); + if desc == null { return 0; } + + msg_oo : (*void, *void, *void) -> *void = xx objc_msgSend; + tex := msg_oo(self.device, sel_registerName("newTextureWithDescriptor:".ptr), desc); + if tex == null { return 0; } + + slot : TextureSlot = .{ tex = tex, bytes_per_pixel = bytes_per_pixel }; + self.textures.append(slot); + + if pixels != null { + handle : u32 = xx self.textures.len; + metal_update_texture_region_ios(self, handle, 0, 0, w, h, pixels); + } + + xx self.textures.len; +} + +metal_update_texture_region_ios :: (self: *MetalGPU, handle: u32, x: s32, y: s32, w: s32, h: s32, pixels: *void) { + inline if OS != .ios { return; } + if handle == 0 { return; } + h64 : s64 = xx handle; + if h64 > self.textures.len { return; } + slot := self.textures.items[handle - 1]; + if slot.tex == null { return; } + if pixels == null { return; } + if w <= 0 { return; } + if h <= 0 { return; } + + region : MTLRegion = .{ + origin = .{ x = xx x, y = xx y, z = 0 }, + size = .{ width = xx w, height = xx h, depth = 1 }, + }; + bytes_per_row : u64 = xx (slot.bytes_per_pixel * cast(u32) w); + + // [tex replaceRegion:region mipmapLevel:0 withBytes:pixels bytesPerRow:bytes_per_row] + msg_replace : (*void, *void, *MTLRegion, u64, *void, u64) -> void = xx objc_msgSend; + msg_replace(slot.tex, + sel_registerName("replaceRegion:mipmapLevel:withBytes:bytesPerRow:".ptr), + @region, 0, pixels, bytes_per_row); +} + +// ── Per-draw state ─────────────────────────────────────────────────────── + +metal_set_shader_ios :: (self: *MetalGPU, sh: u32) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + state := metal_lookup_shader(self, sh); + if state == null { return; } + msg : (*void, *void, *void) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("setRenderPipelineState:".ptr), state); +} + +metal_set_vertex_buffer_ios :: (self: *MetalGPU, h: u32) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + buf := metal_lookup_buffer(self, h); + if buf == null { return; } + // [encoder setVertexBuffer:buf offset:0 atIndex:0] + msg : (*void, *void, *void, u64, u64) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("setVertexBuffer:offset:atIndex:".ptr), buf, 0, 0); +} + +metal_set_texture_ios :: (self: *MetalGPU, slot: u32, h: u32) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + if h == 0 { return; } + h64 : s64 = xx h; + if h64 > self.textures.len { return; } + tex := self.textures.items[h - 1].tex; + if tex == null { return; } + // [encoder setFragmentTexture:tex atIndex:slot] + msg : (*void, *void, *void, u64) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("setFragmentTexture:atIndex:".ptr), tex, xx slot); +} + +metal_set_vertex_constants_ios :: (self: *MetalGPU, slot: u32, data: *void, size_bytes: s64) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + if data == null { return; } + if size_bytes <= 0 { return; } + // [encoder setVertexBytes:data length:size_bytes atIndex:slot] + msg : (*void, *void, *void, u64, u64) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("setVertexBytes:length:atIndex:".ptr), + data, xx size_bytes, xx slot); +} + +metal_set_scissor_ios :: (self: *MetalGPU, x: s32, y: s32, w: s32, h: s32) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + rect : MTLScissorRect = .{ x = xx x, y = xx y, width = xx w, height = xx h }; + // [encoder setScissorRect:rect] (MTLScissorRect is 32 bytes → indirect) + msg : (*void, *void, *MTLScissorRect) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("setScissorRect:".ptr), @rect); +} + +metal_disable_scissor_ios :: (self: *MetalGPU) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + // Metal has no "disable scissor" — set the rect to cover the full + // drawable so subsequent draws aren't clipped. + rect : MTLScissorRect = .{ x = 0, y = 0, width = xx self.pixel_w, height = xx self.pixel_h }; + msg : (*void, *void, *MTLScissorRect) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("setScissorRect:".ptr), @rect); +} + +metal_draw_triangles_ios :: (self: *MetalGPU, vertex_offset: s32, vertex_count: s32) { + inline if OS != .ios { return; } + if self.encoder == null { return; } + if vertex_count <= 0 { return; } + // [encoder drawPrimitives:.triangle vertexStart:offset vertexCount:count] + msg : (*void, *void, u64, u64, u64) -> void = xx objc_msgSend; + msg(self.encoder, sel_registerName("drawPrimitives:vertexStart:vertexCount:".ptr), + MTL_PRIMITIVE_TYPE_TRIANGLE, xx vertex_offset, xx vertex_count); +} diff --git a/library/modules/gpu/types.sx b/library/modules/gpu/types.sx new file mode 100644 index 0000000..4c396e5 --- /dev/null +++ b/library/modules/gpu/types.sx @@ -0,0 +1,24 @@ +#import "modules/std.sx"; + +// Opaque GPU resource handles. Backends decide what the integer means +// (GL: object name; Metal: 1-based index into a backend-owned table of +// retained MTL* objects). Zero is reserved for "no handle". +ShaderHandle :: u32; +BufferHandle :: u32; +TextureHandle :: u32; + +ClearColor :: struct { + r: f32; + g: f32; + b: f32; + a: f32; + + black :: () -> ClearColor => .{ r = 0.0, g = 0.0, b = 0.0, a = 1.0 }; +} + +// Texture pixel format. The UI renderer only needs rgba8 (images, the +// 1×1 white solid-rect texture) and r8 (font glyph atlas alpha). +TextureFormat :: enum { + rgba8; + r8; +} diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index fff5958..0592ac4 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -55,16 +55,26 @@ GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5; g_uikit_plat : *UIKitPlatform = null; +// Which GPU API the UIKit backend wires up. `.gles` keeps the existing +// CAEAGLLayer + EAGLContext + renderbuffer path; `.metal` swaps the view +// for a CAMetalLayer-backed one and leaves rendering to MetalGPU. Set +// before calling `init`; default `.gles` preserves prior behavior. +GpuMode :: enum { + gles; + metal; +} + UIKitPlatform :: struct { window: *void = null; // UIWindow* root_vc: *void = null; // UIViewController* - gl_view: *void = null; // SxGLView* - gl_layer: *void = null; // CAEAGLLayer* (= gl_view.layer) - gl_ctx: *void = null; // EAGLContext* + gl_view: *void = null; // SxGLView* OR SxMetalView* (depending on gpu_mode) + gl_layer: *void = null; // CAEAGLLayer* OR CAMetalLayer* (= gl_view.layer) + gl_ctx: *void = null; // EAGLContext* (null in metal mode) display_link: *void = null; color_renderbuffer: u32 = 0; framebuffer: u32 = 0; gl_initialized: bool = false; + gpu_mode: GpuMode = .gles; // Hidden UITextField; firstResponder ⇆ keyboard visibility. text_field: *void = null; @@ -121,7 +131,13 @@ impl Platform for UIKitPlatform { inline if OS == .ios { uikit_chdir_to_bundle(); uikit_register_classes(); - uikit_create_gl_context(self); + if self.gpu_mode == .gles { + uikit_create_gl_context(self); + } else { + // Metal mode: skip EAGL. dpi_scale still needs to be known + // before the window exists so callers can size resources. + uikit_read_screen_scale(self); + } } true; } @@ -159,7 +175,10 @@ impl Platform for UIKitPlatform { end_frame :: (self: *UIKitPlatform) { inline if OS == .ios { - uikit_present_renderbuffer(self); + if self.gpu_mode == .gles { + uikit_present_renderbuffer(self); + } + // Metal mode: caller's gpu.end_frame() handles present. } } @@ -274,9 +293,25 @@ uikit_register_classes :: () { objc_registerClassPair(SxAppDelegate); uikit_register_gl_view_class(); + uikit_register_metal_view_class(); } } +// Read [UIScreen mainScreen].nativeScale into plat.dpi_scale. Used by the +// metal-mode init path which doesn't go through uikit_create_gl_context +// (that's where the gles path picks the scale up). +uikit_read_screen_scale :: (plat: *UIKitPlatform) { + inline if OS != .ios { return; } + UIScreen := objc_getClass("UIScreen".ptr); + sel_main_screen := sel_registerName("mainScreen".ptr); + sel_native_scale := sel_registerName("nativeScale".ptr); + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_d : (*void, *void) -> f64 = xx objc_msgSend; + screen := msg_o(UIScreen, sel_main_screen); + scale_d : f64 = msg_d(screen, sel_native_scale); + plat.dpi_scale = xx scale_d; +} + // NSNotification callback. The notification's userInfo dict has the // keyboard's end-frame and animation curve/duration. // UIKeyboardFrameEndUserInfoKey → NSValue wrapping CGRect (screen coords) @@ -390,6 +425,7 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { UIWindow := objc_getClass("UIWindow".ptr); UIViewController := objc_getClass("UIViewController".ptr); SxGLView := objc_getClass("SxGLView".ptr); + SxMetalView := objc_getClass("SxMetalView".ptr); EAGLContext := objc_getClass("EAGLContext".ptr); CADisplayLink := objc_getClass("CADisplayLink".ptr); NSRunLoop := objc_getClass("NSRunLoop".ptr); @@ -445,11 +481,12 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { vc_raw := msg_o(UIViewController, sel_alloc); plat.root_vc = msg_o(vc_raw, sel_init); - // Allocate SxGLView and install it as the VC's view, so the standard - // ViewController layout pipeline sizes the GL view to the window. Setting - // it BEFORE setRootViewController avoids the VC lazy-loading a default - // view first. - glv_raw := msg_o(SxGLView, sel_alloc); + // Allocate either SxGLView or SxMetalView based on gpu_mode and install + // it as the VC's view. The view's +layerClass override gives us the + // right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE + // setRootViewController avoids the VC lazy-loading a default view. + view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView; + glv_raw := msg_o(view_class, sel_alloc); plat.gl_view = msg_o(glv_raw, sel_init); sel_set_view := sel_registerName("setView:".ptr); msg_oo(plat.root_vc, sel_set_view, plat.gl_view); @@ -458,35 +495,40 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { plat.gl_layer = msg_o(plat.gl_view, sel_layer); - // Mark the layer opaque (no compositor blend) + set the drawable properties - // required by EAGLContext.renderbufferStorage:fromDrawable: (color format, - // non-retained backing). Without this dict the renderbuffer allocation - // silently fails and the framebuffer reports INCOMPLETE. + // Mark the layer opaque (no compositor blend). Required for EAGL + + // recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't + // hurt to be explicit). sel_set_opaque := sel_registerName("setOpaque:".ptr); msg_obool : (*void, *void, u8) -> void = xx objc_msgSend; msg_obool(plat.gl_layer, sel_set_opaque, 1); - NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr); - NSNumber := objc_getClass("NSNumber".ptr); - sel_dictionary := sel_registerName("dictionary".ptr); - sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr); - sel_number_bool := sel_registerName("numberWithBool:".ptr); - sel_set_drawable := sel_registerName("setDrawableProperties:".ptr); + if plat.gpu_mode == .gles { + // EAGL drawable properties dict required by + // EAGLContext.renderbufferStorage:fromDrawable: (color format, + // non-retained backing). Without this dict the renderbuffer + // allocation silently fails and the framebuffer reports INCOMPLETE. + NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr); + NSNumber := objc_getClass("NSNumber".ptr); + sel_dictionary := sel_registerName("dictionary".ptr); + sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr); + sel_number_bool := sel_registerName("numberWithBool:".ptr); + sel_set_drawable := sel_registerName("setDrawableProperties:".ptr); - msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend; - ns_no := msg_oio(NSNumber, sel_number_bool, 0); + msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend; + ns_no := msg_oio(NSNumber, sel_number_bool, 0); - // The EAGL dict keys/values must be the framework-provided NSString - // constants (pointer identity is checked) — dlsym them from OpenGLES. - retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr); - colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr); - rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr); + // The EAGL dict keys/values must be the framework-provided NSString + // constants (pointer identity is checked) — dlsym them from OpenGLES. + retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr); + colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr); + rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr); - dict := msg_o(NSMutableDictionary, sel_dictionary); - msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend; - msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key); - msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key); - msg_oo(plat.gl_layer, sel_set_drawable, dict); + dict := msg_o(NSMutableDictionary, sel_dictionary); + msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend; + msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key); + msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key); + msg_oo(plat.gl_layer, sel_set_drawable, dict); + } // EAGLContext + load_gl were already done in uikit_create_gl_context() // back when the game's main called plat.init() — so shaders/textures @@ -615,10 +657,38 @@ uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) { if g_uikit_plat == null { return; } plat := g_uikit_plat; if plat.gl_initialized { return; } - uikit_setup_renderbuffer(plat); + if plat.gpu_mode == .gles { + uikit_setup_renderbuffer(plat); + } else { + uikit_compute_layer_pixel_size(plat); + } plat.gl_initialized = true; } +// Metal mode equivalent of uikit_setup_renderbuffer's "tell me how big the +// drawable is in pixels". Reads the layer's bounds in points and scales to +// pixels via dpi_scale. CAMetalLayer.drawableSize is set by MetalGPU.init +// based on these dims. +uikit_compute_layer_pixel_size :: (plat: *UIKitPlatform) { + inline if OS != .ios { return; } + if plat.gl_view == null { return; } + + sel_bounds := sel_registerName("bounds".ptr); + msg_rect : (*void, *void) -> CGRect = xx objc_msgSend; + b := msg_rect(plat.gl_view, sel_bounds); + + w_pts : f64 = b.width; + h_pts : f64 = b.height; + plat.viewport_w = xx w_pts; + plat.viewport_h = xx h_pts; + + scale64 : f64 = xx plat.dpi_scale; + pw : f64 = w_pts * scale64; + ph : f64 = h_pts * scale64; + plat.pixel_w = xx pw; + plat.pixel_h = xx ph; +} + // Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an // NSSet + UIEvent. We take the first touch (single-touch model // matching the chess game's drag-and-tap UX) and push the resulting @@ -698,3 +768,45 @@ uikit_register_gl_view_class :: () { objc_registerClassPair(SxGLView); } } + +// +layerClass IMP for SxMetalView. Class method, signature "#@:". +uikit_metal_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) { + objc_getClass("CAMetalLayer".ptr); +} + +// SxMetalView reuses the same tick/layout/touch IMPs as SxGLView. The IMPs +// already branch on `plat.gpu_mode` for the GL-specific bits (renderbuffer +// setup, etc.), so a single set of IMPs serves both view classes. +uikit_register_metal_view_class :: () { + inline if OS == .ios { + UIView := objc_getClass("UIView".ptr); + SxMetalView := objc_allocateClassPair(UIView, "SxMetalView".ptr, 0); + + metaclass := object_getClass(SxMetalView); + class_addMethod(metaclass, + sel_registerName("layerClass".ptr), + xx uikit_metal_view_layer_class, "#@:".ptr); + + class_addMethod(SxMetalView, + sel_registerName("sxTick:".ptr), + xx uikit_gl_view_tick, "v@:@".ptr); + class_addMethod(SxMetalView, + sel_registerName("layoutSubviews".ptr), + xx uikit_gl_view_layout, "v@:".ptr); + + class_addMethod(SxMetalView, + sel_registerName("touchesBegan:withEvent:".ptr), + xx uikit_gl_view_touches_began, "v@:@@".ptr); + class_addMethod(SxMetalView, + sel_registerName("touchesMoved:withEvent:".ptr), + xx uikit_gl_view_touches_moved, "v@:@@".ptr); + class_addMethod(SxMetalView, + sel_registerName("touchesEnded:withEvent:".ptr), + xx uikit_gl_view_touches_ended, "v@:@@".ptr); + class_addMethod(SxMetalView, + sel_registerName("touchesCancelled:withEvent:".ptr), + xx uikit_gl_view_touches_ended, "v@:@@".ptr); + + objc_registerClassPair(SxMetalView); + } +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 4a5dedd..535e1a1 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -2848,19 +2848,29 @@ pub const LLVMEmitter = struct { } fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { - const elem_ty = c.LLVMGetElementType(llvm_ty); + const kind = c.LLVMGetTypeKind(llvm_ty); + const is_struct = kind == c.LLVMStructTypeKind; const n: c_uint = @intCast(agg.len); const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty); defer self.alloc.free(vals); for (agg, 0..) |cv, i| { + const elem_ty = if (is_struct) + c.LLVMStructGetTypeAtIndex(llvm_ty, @intCast(i)) + else + c.LLVMGetElementType(llvm_ty); vals[i] = switch (cv) { .int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1), .float => |v| c.LLVMConstReal(elem_ty, v), .boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0), + .string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)), .aggregate => |inner| self.emitConstAggregate(inner, elem_ty), else => c.LLVMConstNull(elem_ty), }; } + if (is_struct) { + return c.LLVMConstNamedStruct(llvm_ty, vals.ptr, n); + } + const elem_ty = c.LLVMGetElementType(llvm_ty); return c.LLVMConstArray(elem_ty, vals.ptr, n); } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5405120..cf1f097 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -447,10 +447,9 @@ pub const Lowering = struct { }, .var_decl => |vd| { // Top-level mutable global (e.g., `context : Context = ---;`) - const var_ty = if (vd.type_annotation) |ta| - type_bridge.resolveAstType(ta, &self.module.types) - else - .s64; + // Use self.resolveType so type aliases like `Handle :: u32;` resolve + // to their target type (not a synthetic empty struct). + const var_ty = self.resolveType(vd.type_annotation); const name_id = self.module.types.internString(vd.name); const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) { .undef_literal => .zeroinit, @@ -459,6 +458,7 @@ pub const Lowering = struct { .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .array_literal => |al| self.constArrayLiteral(al.elements), + .struct_literal => |sl| self.constStructLiteral(&sl, var_ty), else => null, } else null; const gid = self.module.addGlobal(.{ @@ -479,22 +479,67 @@ pub const Lowering = struct { fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue { const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; for (elements, 0..) |elem, i| { - vals[i] = switch (elem.data) { - .int_literal => |il| .{ .int = il.value }, - .bool_literal => |bl| .{ .boolean = bl.value }, - .float_literal => |fl| .{ .float = fl.value }, - .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, - .unary_op => |uo| switch (uo.op) { - .negate => switch (uo.operand.data) { - .int_literal => |il| .{ .int = -il.value }, - .float_literal => |fl| .{ .float = -fl.value }, - else => return null, - }, - else => return null, + vals[i] = self.constExprValue(elem) orelse return null; + } + return .{ .aggregate = vals }; + } + + /// Try to convert a single AST expression into a compile-time ConstantValue. + /// Returns null if the expression is not constant-foldable here. + fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue { + return switch (expr.data) { + .int_literal => |il| .{ .int = il.value }, + .bool_literal => |bl| .{ .boolean = bl.value }, + .float_literal => |fl| .{ .float = fl.value }, + .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, + .undef_literal => .zeroinit, + .unary_op => |uo| switch (uo.op) { + .negate => switch (uo.operand.data) { + .int_literal => |il| .{ .int = -il.value }, + .float_literal => |fl| .{ .float = -fl.value }, + else => null, }, - .array_literal => |al| self.constArrayLiteral(al.elements) orelse return null, - else => return null, + else => null, + }, + .array_literal => |al| self.constArrayLiteral(al.elements), + else => null, + }; + } + + /// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the + /// struct's fields in declaration order, filling missing fields from the struct's + /// field defaults. Returns null if any value is not constant-foldable. + fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue { + if (ty.isBuiltin()) return null; + const ti = self.module.types.get(ty); + if (ti != .@"struct") return null; + const struct_fields = ti.@"struct".fields; + const struct_name = self.module.types.getString(ti.@"struct".name); + const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{}; + + const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; + + const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null; + for (struct_fields, 0..) |sf, fi| { + const sf_name = self.module.types.getString(sf.name); + const init_expr: ?*const Node = blk: { + if (has_names) { + for (sl.field_inits) |init_pair| { + if (init_pair.name) |n| { + if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value; + } + } + } else if (fi < sl.field_inits.len) { + break :blk sl.field_inits[fi].value; + } + if (fi < field_defaults.len) break :blk field_defaults[fi]; + break :blk null; }; + if (init_expr) |e| { + vals[fi] = self.constExprValue(e) orelse return null; + } else { + vals[fi] = .zeroinit; + } } return .{ .aggregate = vals }; } @@ -862,6 +907,12 @@ pub const Lowering = struct { fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref { if (node.data == .block) { self.lowerBlock(node); + // A `return` inside the branch terminates the current LLVM block; propagate + // that up so the enclosing block lowering stops emitting fall-through. + if (self.currentBlockHasTerminator()) { + self.block_terminated = true; + return .none; + } return self.builder.constInt(0, .void); } return self.lowerExpr(node); @@ -1152,12 +1203,13 @@ pub const Lowering = struct { const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object)); if (elem_ty != .void) self.target_type = elem_ty; } else if (asgn.target.data == .field_access) { - // For obj.field = val, set target_type to the field's type - // Only for enum literals and struct literals — these need target_type to resolve. - // Avoid setting it for complex RHS expressions (calls, casts) because - // resolveCallParamTypes can't override target_type for method-call args. + // For obj.field = val, set target_type to the field's type so RHS + // sub-expressions (enum/struct literals, branch arms, xx casts) can + // resolve against it. Skipped for forms that would forward the type + // unchanged into method-call arg slots (`resolveCallParamTypes` can't + // override target_type per-arg). const needs_target = switch (asgn.value.data) { - .enum_literal, .struct_literal => true, + .enum_literal, .struct_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true, .call => |vc| vc.callee.data == .enum_literal, else => false, }; @@ -1266,7 +1318,7 @@ pub const Lowering = struct { for (fields, 0..) |f, i| { if (f.name == field_name_id) { const gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); - const src_ty = self.inferExprType(asgn.value); + const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, f.ty); self.storeOrCompound(gep, coerced, asgn.op, f.ty); return; @@ -1280,7 +1332,7 @@ pub const Lowering = struct { // GEP into union payload area, then into the struct field const union_gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); const field_gep = self.builder.structGepTyped(union_gep, @intCast(si), sf.ty, f.ty); - const src_ty = self.inferExprType(asgn.value); + const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, sf.ty); self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty); return; @@ -1308,8 +1360,9 @@ pub const Lowering = struct { // target type, storing element-sized bytes instead of a pointer. const gep_ty = self.module.types.ptrTo(field_ty); const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty); - // Coerce value to field type - const src_ty = self.inferExprType(asgn.value); + // Coerce value to field type — use the lowered value's actual type + // (not inferExprType, which can re-read target_type after restore). + const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, field_ty); self.storeOrCompound(gep, coerced, asgn.op, field_ty); } diff --git a/tests/expected/67-impl-for-builtin.exit b/tests/expected/67-impl-for-builtin.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/67-impl-for-builtin.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/67-impl-for-builtin.txt b/tests/expected/67-impl-for-builtin.txt new file mode 100644 index 0000000..f0af75c --- /dev/null +++ b/tests/expected/67-impl-for-builtin.txt @@ -0,0 +1,2 @@ +lerp(0, 10, 0.5) = 5.000000 +lerp(0, 10, 0.25) = 2.500000 diff --git a/tests/expected/68-generic-protocol-constraint.exit b/tests/expected/68-generic-protocol-constraint.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/68-generic-protocol-constraint.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/68-generic-protocol-constraint.txt b/tests/expected/68-generic-protocol-constraint.txt new file mode 100644 index 0000000..0c5de4f --- /dev/null +++ b/tests/expected/68-generic-protocol-constraint.txt @@ -0,0 +1,3 @@ +after set: 100.000000x50.000000 +mid anim: 150.000000x75.000000 +end anim: 200.000000x100.000000 diff --git a/tests/expected/69-optional-all-null.exit b/tests/expected/69-optional-all-null.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/69-optional-all-null.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/69-optional-all-null.txt b/tests/expected/69-optional-all-null.txt new file mode 100644 index 0000000..5e18081 --- /dev/null +++ b/tests/expected/69-optional-all-null.txt @@ -0,0 +1,3 @@ +r1 = 150.000000 +r2 = 150.000000 +r3 = 200.000000 diff --git a/tests/expected/70-optional-roundtrip.exit b/tests/expected/70-optional-roundtrip.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/70-optional-roundtrip.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/70-optional-roundtrip.txt b/tests/expected/70-optional-roundtrip.txt new file mode 100644 index 0000000..9829a3e --- /dev/null +++ b/tests/expected/70-optional-roundtrip.txt @@ -0,0 +1,10 @@ +=== Direct calls === +d1 = 150.000000 +d2 = 150.000000 +d3 = 200.000000 +d4 = 110.000000 +=== Protocol dispatch === +r1 = 150.000000 +r2 = 150.000000 +r3 = 200.000000 +r4 = 110.000000 diff --git a/tests/expected/71-int-cmp-in-float-ternary.exit b/tests/expected/71-int-cmp-in-float-ternary.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/71-int-cmp-in-float-ternary.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/71-int-cmp-in-float-ternary.txt b/tests/expected/71-int-cmp-in-float-ternary.txt new file mode 100644 index 0000000..0f7b189 --- /dev/null +++ b/tests/expected/71-int-cmp-in-float-ternary.txt @@ -0,0 +1,2 @@ +ok +result = 1.000000 diff --git a/tests/expected/72-protocol-in-wrapper-struct.exit b/tests/expected/72-protocol-in-wrapper-struct.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/72-protocol-in-wrapper-struct.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/72-protocol-in-wrapper-struct.txt b/tests/expected/72-protocol-in-wrapper-struct.txt new file mode 100644 index 0000000..d1db6da --- /dev/null +++ b/tests/expected/72-protocol-in-wrapper-struct.txt @@ -0,0 +1,4 @@ +inside add: 42 +inside add: 99 +items[0] = 42 (expected 42) +items[1] = 99 (expected 99) diff --git a/tests/expected/73-protocol-list-from-fn.exit b/tests/expected/73-protocol-list-from-fn.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/73-protocol-list-from-fn.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/73-protocol-list-from-fn.txt b/tests/expected/73-protocol-list-from-fn.txt new file mode 100644 index 0000000..3f32550 --- /dev/null +++ b/tests/expected/73-protocol-list-from-fn.txt @@ -0,0 +1,7 @@ +=== Created in main === +first: 42 (expected 42) +second: 42 (expected 42) +=== Created in add() === +first: 99 (expected 99) +second: 99 (expected 99) +=== OK === diff --git a/tests/expected/74-protocol-dispatch-via-fn-arg.exit b/tests/expected/74-protocol-dispatch-via-fn-arg.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/74-protocol-dispatch-via-fn-arg.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/74-protocol-dispatch-via-fn-arg.txt b/tests/expected/74-protocol-dispatch-via-fn-arg.txt new file mode 100644 index 0000000..34b9d44 --- /dev/null +++ b/tests/expected/74-protocol-dispatch-via-fn-arg.txt @@ -0,0 +1,8 @@ +=== Direct 1 === +r1 = 42 +=== Direct 2 === +r2 = 42 +=== From function === +dispatch_fn: about to dispatch +dispatch_fn: result = 42 +=== OK === diff --git a/tests/expected/75-push-context-with-arena.exit b/tests/expected/75-push-context-with-arena.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/75-push-context-with-arena.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/75-push-context-with-arena.txt b/tests/expected/75-push-context-with-arena.txt new file mode 100644 index 0000000..55b0466 --- /dev/null +++ b/tests/expected/75-push-context-with-arena.txt @@ -0,0 +1,2 @@ +inside push +after push diff --git a/tests/expected/76-closure-returning-protocol.exit b/tests/expected/76-closure-returning-protocol.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/76-closure-returning-protocol.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/76-closure-returning-protocol.txt b/tests/expected/76-closure-returning-protocol.txt new file mode 100644 index 0000000..ed78061 --- /dev/null +++ b/tests/expected/76-closure-returning-protocol.txt @@ -0,0 +1,2 @@ +direct call works +closure call works diff --git a/tests/expected/77-list-items-assign-big-T.exit b/tests/expected/77-list-items-assign-big-T.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/77-list-items-assign-big-T.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/77-list-items-assign-big-T.txt b/tests/expected/77-list-items-assign-big-T.txt new file mode 100644 index 0000000..f0f08c5 --- /dev/null +++ b/tests/expected/77-list-items-assign-big-T.txt @@ -0,0 +1,4 @@ +size_of BigNode = 40 +before: sentinel = 3735928559 +after: sentinel = 3735928559 +OK diff --git a/tests/expected/78-global-compound-assign.exit b/tests/expected/78-global-compound-assign.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/78-global-compound-assign.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/78-global-compound-assign.txt b/tests/expected/78-global-compound-assign.txt new file mode 100644 index 0000000..639e149 --- /dev/null +++ b/tests/expected/78-global-compound-assign.txt @@ -0,0 +1,10 @@ +--- Test 1: += (broken) --- +Expected: 1, 2, 3 +counter=1 +counter=2 +counter=3 +--- Test 2: = x + 1 (works) --- +Expected: 2, 3, 4 +counter=4 +counter=5 +counter=6 diff --git a/tests/expected/79-global-array-init.exit b/tests/expected/79-global-array-init.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/79-global-array-init.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/79-global-array-init.txt b/tests/expected/79-global-array-init.txt new file mode 100644 index 0000000..8138ede --- /dev/null +++ b/tests/expected/79-global-array-init.txt @@ -0,0 +1,2 @@ +VALS: -2 -1 42 99 +PASS diff --git a/tests/expected/80-dot-shorthand-protocol-field.exit b/tests/expected/80-dot-shorthand-protocol-field.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/80-dot-shorthand-protocol-field.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/80-dot-shorthand-protocol-field.txt b/tests/expected/80-dot-shorthand-protocol-field.txt new file mode 100644 index 0000000..de608f0 --- /dev/null +++ b/tests/expected/80-dot-shorthand-protocol-field.txt @@ -0,0 +1,3 @@ +StackA: draw=42 +StackB: draw=25 +OK diff --git a/tests/expected/81-global-struct-defaults.exit b/tests/expected/81-global-struct-defaults.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/81-global-struct-defaults.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/81-global-struct-defaults.txt b/tests/expected/81-global-struct-defaults.txt new file mode 100644 index 0000000..5cc3d7c --- /dev/null +++ b/tests/expected/81-global-struct-defaults.txt @@ -0,0 +1,6 @@ +local running=true x=42 name=default +g_empty running=true x=42 name=default +g_partial running=true x=99 name=default +g_override running=false x=42 name=default +g_reorder running=false x=7 name=hi +g_positional running=false x=13 name=pos diff --git a/tests/expected/82-xx-target-in-field-assign.exit b/tests/expected/82-xx-target-in-field-assign.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/82-xx-target-in-field-assign.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/82-xx-target-in-field-assign.txt b/tests/expected/82-xx-target-in-field-assign.txt new file mode 100644 index 0000000..4a646b0 --- /dev/null +++ b/tests/expected/82-xx-target-in-field-assign.txt @@ -0,0 +1,2 @@ +dpi=2.000000 +delta=0.500000 fc.a=1.000000 diff --git a/tests/expected/83-inline-if-return-fallthrough.exit b/tests/expected/83-inline-if-return-fallthrough.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/83-inline-if-return-fallthrough.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/83-inline-if-return-fallthrough.txt b/tests/expected/83-inline-if-return-fallthrough.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/expected/83-inline-if-return-fallthrough.txt @@ -0,0 +1 @@ + diff --git a/tests/expected/84-global-type-alias.exit b/tests/expected/84-global-type-alias.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/84-global-type-alias.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/84-global-type-alias.txt b/tests/expected/84-global-type-alias.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/expected/84-global-type-alias.txt @@ -0,0 +1 @@ +