diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 7897cb3..9759e45 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -296,6 +296,13 @@ Builder :: struct { } } +// Global variable for address-of test +g_smoke_val : s32 = 42; + +write_to_ptr :: (p: *s32) { + p.* = 99; +} + main :: () { // ======================================================== @@ -724,6 +731,10 @@ END; print("str-suffix: {}\n", msg[6..]); // --- Pointers --- + // Address-of global variable + write_to_ptr(@g_smoke_val); + print("global-addr-of: {}\n", g_smoke_val); + pv := Point.{ 10, 20 }; ptr := @pv; print("deref: {}\n", ptr.*); @@ -750,6 +761,13 @@ END; print("ptr2==null: {}\n", np2 == null); print("ptr2!=null: {}\n", np2 != null); + // Pointer to nested struct field + Inner3 :: struct { a: f32; b: f32; c: f32; } + Outer3 :: struct { key: s32; inner: Inner3; } + out3 := Outer3.{ key = 42, inner = Inner3.{ a = 1.0, b = 2.0, c = 3.0 } }; + ip3 := @out3.inner; + print("ptr-nested-field: {} {} {}\n", ip3.a, ip3.b, ip3.c); + // --- Vectors --- vc := vec3(1, 3, 2); print("vec-construct: {}\n", vc); @@ -1291,6 +1309,34 @@ END; print("3-way: {} {} {}\n", ra, rb, rc); } + // --- Tuple destructuring --- + print("--- destructure ---\n"); + + // Basic tuple destructuring + { + da, db := (10, 20); + print("basic: {} {}\n", da, db); + } + + // Destructure from function return + { + dswap :: (a: s64, b: s64) -> (s64, s64) { (b, a); } + dx, dy := dswap(1, 2); + print("fn: {} {}\n", dx, dy); + } + + // Discard with _ + { + _, dsecond := (100, 200); + print("discard: {}\n", dsecond); + } + + // Three elements + { + da3, db3, dc3 := (1, 2, 3); + print("triple: {} {} {}\n", da3, db3, dc3); + } + // ======================================================== // 15. FOREIGN FUNCTION BINDING // ======================================================== diff --git a/examples/modules/compiler.sx b/examples/modules/compiler.sx index bf1ab32..5893ac9 100644 --- a/examples/modules/compiler.sx +++ b/examples/modules/compiler.sx @@ -8,6 +8,7 @@ POINTER_SIZE : s64 = 8; BuildOptions :: struct { add_link_flag :: (self: BuildOptions, flag: [:0]u8) #compiler; set_output_path :: (self: BuildOptions, path: [:0]u8) #compiler; + set_wasm_shell :: (self: BuildOptions, path: [:0]u8) #compiler; } build_options :: () -> BuildOptions #compiler; diff --git a/examples/modules/opengl.sx b/examples/modules/opengl.sx index 2a4c368..a70b11a 100644 --- a/examples/modules/opengl.sx +++ b/examples/modules/opengl.sx @@ -86,6 +86,12 @@ glReadPixels : (s32, s32, s32, s32, u32, u32, *void) -> void = ---; glActiveTexture : (u32) -> void = ---; glUniform1i : (s32, s32) -> void = ---; glPixelStorei : (u32, s32) -> void = ---; +glTexSubImage2D : (u32, s32, s32, s32, s32, s32, u32, u32, *void) -> void = ---; +glDeleteTextures : (s32, *u32) -> void = ---; + +GL_TEXTURE_WRAP_S :u32: 0x2802; +GL_TEXTURE_WRAP_T :u32: 0x2803; +GL_CLAMP_TO_EDGE :u32: 0x812F; // Loader: call once after creating GL context // Pass in a proc loader (e.g. SDL_GL_GetProcAddress) @@ -133,6 +139,8 @@ load_gl :: (get_proc: ([*]u8) -> *void) { glActiveTexture = xx get_proc("glActiveTexture"); glUniform1i = xx get_proc("glUniform1i"); glPixelStorei = xx get_proc("glPixelStorei"); + glTexSubImage2D = xx get_proc("glTexSubImage2D"); + glDeleteTextures = xx get_proc("glDeleteTextures"); } diff --git a/examples/modules/sdl3.sx b/examples/modules/sdl3.sx index 678031c..923c099 100644 --- a/examples/modules/sdl3.sx +++ b/examples/modules/sdl3.sx @@ -4,7 +4,9 @@ sdl3 :: #library "SDL3"; SDL_INIT_VIDEO :u32: 0x20; // SDL_WindowFlags -SDL_WINDOW_OPENGL :u64: 0x2; +SDL_WINDOW_OPENGL :u64: 0x2; +SDL_WINDOW_RESIZABLE :u64: 0x20; +SDL_WINDOW_HIGH_PIXEL_DENSITY :u64: 0x2000; // SDL_GLAttr (enum starting at 0) SDL_GL_DOUBLEBUFFER :s32: 5; @@ -332,3 +334,16 @@ SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign sdl3; SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign sdl3; SDL_GetTicks :: () -> u64 #foreign sdl3; SDL_Delay :: (ms: u32) -> void #foreign sdl3; +SDL_GetWindowDisplayScale :: (window: *void) -> f32 #foreign sdl3; +SDL_GetWindowSize :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; +SDL_GetWindowSizeInPixels :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; + +SDL_Rect :: struct { + x: s32; + y: s32; + w: s32; + h: s32; +} + +SDL_GetPrimaryDisplay :: () -> u32 #foreign sdl3; +SDL_GetDisplayUsableBounds :: (display_id: u32, rect: *SDL_Rect) -> bool #foreign sdl3; diff --git a/examples/modules/stb_truetype.sx b/examples/modules/stb_truetype.sx index 7ee5834..b1cc40b 100644 --- a/examples/modules/stb_truetype.sx +++ b/examples/modules/stb_truetype.sx @@ -4,4 +4,7 @@ #include "vendors/file_utils/file_utils.h"; #source "vendors/file_utils/file_utils.c"; + + #include "vendors/kb_text_shape/kbts_api.h"; + #source "vendors/kb_text_shape/kb_text_shape_impl.c"; }; diff --git a/src/ast.zig b/src/ast.zig index 6a34b65..38c2818 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -32,6 +32,7 @@ pub const Node = struct { var_decl: VarDecl, assignment: Assignment, multi_assign: MultiAssign, + destructure_decl: DestructureDecl, enum_decl: EnumDecl, struct_decl: StructDecl, struct_literal: StructLiteral, @@ -263,6 +264,11 @@ pub const MultiAssign = struct { values: []const *Node, }; +pub const DestructureDecl = struct { + names: []const []const u8, + value: *Node, +}; + pub const EnumDecl = struct { name: []const u8, variant_names: []const []const u8, diff --git a/src/core.zig b/src/core.zig index fbf8908..b771706 100644 --- a/src/core.zig +++ b/src/core.zig @@ -118,6 +118,12 @@ pub const Compilation = struct { return null; } + /// Get custom WASM shell template path set from #run build blocks, if any. + pub fn getBuildWasmShell(self: *Compilation) ?[]const u8 { + if (self.ir_emitter) |*e| return e.build_config.wasm_shell_path; + return null; + } + /// Collect C import source info from the resolved AST. pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo { const root = self.resolved_root orelse self.root orelse return &.{}; diff --git a/src/ir/compiler_hooks.zig b/src/ir/compiler_hooks.zig index a27393d..eed66ff 100644 --- a/src/ir/compiler_hooks.zig +++ b/src/ir/compiler_hooks.zig @@ -10,6 +10,7 @@ const Interpreter = interp_mod.Interpreter; pub const BuildConfig = struct { link_flags: std.ArrayList([]const u8) = .empty, output_path: ?[]const u8 = null, + wasm_shell_path: ?[]const u8 = null, pub fn deinit(self: *BuildConfig, alloc: Allocator) void { self.link_flags.deinit(alloc); @@ -52,6 +53,7 @@ pub const Registry = struct { self.hooks.put("build_options", &hookBuildOptions) catch {}; self.hooks.put("BuildOptions.add_link_flag", &hookAddLinkFlag) catch {}; self.hooks.put("BuildOptions.set_output_path", &hookSetOutputPath) catch {}; + self.hooks.put("BuildOptions.set_wasm_shell", &hookSetWasmShell) catch {}; } }; @@ -99,3 +101,18 @@ fn hookSetOutputPath( } return .void_val; } + +fn hookSetWasmShell( + interp: *const Interpreter, + args: []const Value, + bc: *BuildConfig, + alloc: Allocator, +) HookError!Value { + // args: [self (BuildOptions value), path_string] + if (args.len < 2) return .void_val; + const str_val = args[1]; + if (str_val.asString(interp)) |s| { + bc.wasm_shell_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; + } + return .void_val; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 9eee79c..930a946 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -728,6 +728,14 @@ pub const LLVMEmitter = struct { const llvm_ty = self.toLLVMType(instruction.ty); self.mapRef(c.LLVMBuildLoad2(self.builder, llvm_ty, llvm_global, "gload")); }, + .global_addr => |gid| { + const llvm_global = self.global_map.get(gid.index()) orelse { + self.mapRef(c.LLVMGetUndef(self.cached_ptr)); + return; + }; + // Return the global's address directly (no load) + self.mapRef(llvm_global); + }, .func_ref => |fid| { // Produce a reference to the function as a function pointer value if (self.func_map.get(@intFromEnum(fid))) |llvm_func| { diff --git a/src/ir/inst.zig b/src/ir/inst.zig index b2985f3..1cc3062 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -194,6 +194,7 @@ pub const Op = union(enum) { // ── Globals ───────────────────────────────────────────────────── global_get: GlobalId, + global_addr: GlobalId, // address of a global (pointer, not load) global_set: GlobalSet, func_ref: FuncId, // reference to a function (for function pointers) diff --git a/src/ir/interp.zig b/src/ir/interp.zig index a406ef8..1b0deff 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -872,6 +872,10 @@ pub const Interpreter = struct { const val = try self.getGlobal(gid); return .{ .value = val }; }, + .global_addr => { + // Address-of-global not meaningful in interpreter + return error.CannotEvalComptime; + }, .func_ref => |fid| { return .{ .value = .{ .func_ref = fid } }; }, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2f2f1b9..5813310 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -842,7 +842,7 @@ pub const Lowering = struct { /// Statement nodes are lowered as statements (returning null). fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { return switch (node.data) { - .var_decl, .const_decl, .fn_decl, .return_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign => { + .var_decl, .const_decl, .fn_decl, .return_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => { self.lowerStmt(node); return null; }, @@ -860,6 +860,7 @@ pub const Lowering = struct { .defer_stmt => |ds| self.lowerDefer(&ds), .push_stmt => |ps| self.lowerPush(&ps), .multi_assign => |ma| self.lowerMultiAssign(&ma), + .destructure_decl => |dd| self.lowerDestructureDecl(&dd), .insert_expr => |ins| self.lowerInsertExpr(ins.expr), .block => self.lowerBlock(node), // Block-local type declarations @@ -1492,29 +1493,13 @@ pub const Lowering = struct { const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExpr(ie.object)) else self.lowerExpr(ie.object); break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); } - // address_of(field_access) → emit struct_gep (pointer to field) when object is a pointer + // address_of(field_access) → use lowerExprAsPtr for GEP chain + // Handles all cases: pointer-based, index-based, nested field access if (uop.op == .address_of and uop.operand.data == .field_access) { - const fa = &uop.operand.data.field_access; - const obj_ty = self.inferExprType(fa.object); - if (!obj_ty.isBuiltin()) { - const ptr_info = self.module.types.get(obj_ty); - if (ptr_info == .pointer) { - const pointee = ptr_info.pointer.pointee; - if (!pointee.isBuiltin()) { - const struct_info = self.module.types.get(pointee); - if (struct_info == .@"struct") { - const field_name_id = self.module.types.internString(fa.field); - for (struct_info.@"struct".fields, 0..) |f, fi| { - if (f.name == field_name_id) { - const obj = self.lowerExpr(fa.object); - const field_ptr_ty = self.module.types.ptrTo(f.ty); - break :blk self.builder.structGepTyped(obj, @intCast(fi), field_ptr_ty, pointee); - } - } - } - } - } - } + const inner_ty = self.inferExprType(uop.operand); + const ptr_ty = self.module.types.ptrTo(inner_ty); + const ptr = self.lowerExprAsPtr(uop.operand); + break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty); } // address_of(identifier) → return alloca directly (pointer to variable) if (uop.op == .address_of and uop.operand.data == .identifier) { @@ -1527,6 +1512,11 @@ pub const Lowering = struct { } } } + // address_of(global) → emit global_addr (pointer to global, not load) + if (self.global_names.get(id_name)) |gi| { + const ptr_ty = self.module.types.ptrTo(gi.ty); + break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); + } } const operand = self.lowerExpr(uop.operand); break :blk switch (uop.op) { @@ -4528,6 +4518,12 @@ pub const Lowering = struct { self.collectCaptures(a.target, param_names, captures); self.collectCaptures(a.value, param_names, captures); }, + .destructure_decl => |dd| { + self.collectCaptures(dd.value, param_names, captures); + for (dd.names) |name| { + param_names.put(name, {}) catch {}; + } + }, .field_access => |fa| { self.collectCaptures(fa.object, param_names, captures); }, @@ -4813,6 +4809,38 @@ pub const Lowering = struct { } } + fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void { + // Lower the RHS expression (must produce a tuple) + const saved_fbv = self.force_block_value; + self.force_block_value = true; + const ref = self.lowerExpr(dd.value); + self.force_block_value = saved_fbv; + const ty = self.builder.getRefType(ref); + + // Get tuple field info + if (ty.isBuiltin()) return; + const ti = self.module.types.get(ty); + if (ti != .tuple) return; + const tuple = ti.tuple; + if (dd.names.len > tuple.fields.len) return; + + // Extract each field and bind to a new variable + for (dd.names, 0..) |name, i| { + if (std.mem.eql(u8, name, "_")) continue; // discard + const field_ty = tuple.fields[i]; + const field_val = self.builder.emit(.{ .tuple_get = .{ + .base = ref, + .field_index = @intCast(i), + .base_type = ty, + } }, field_ty); + const slot = self.builder.alloca(field_ty); + self.builder.store(slot, field_val); + if (self.scope) |scope| { + scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true }); + } + } + } + // ── Comptime lowering ──────────────────────────────────────────── /// Lower a `#run expr` that appears as a top-level constant binding: @@ -7767,7 +7795,7 @@ pub const Lowering = struct { .chained_comparison => .bool, // Statements don't produce values .assignment, .var_decl, .const_decl, .fn_decl, .return_stmt, - .defer_stmt, .push_stmt, .multi_assign, + .defer_stmt, .push_stmt, .multi_assign, .destructure_decl, => .void, else => .s64, }; diff --git a/src/ir/print.zig b/src/ir/print.zig index 74e37de..cb665da 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -367,6 +367,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write // ── Globals ───────────────────────────────────────────── .global_get => |gid| try writer.print("global_get @{d} : ", .{gid.index()}), + .global_addr => |gid| try writer.print("global_addr @{d} : ", .{gid.index()}), .func_ref => |fid| try writer.print("func_ref @{d} : ", .{@intFromEnum(fid)}), .global_set => |gs| { try writer.print("global_set @{d}, %{d}\n", .{ gs.global.index(), gs.value.index() }); diff --git a/src/main.zig b/src/main.zig index a4999d5..511705b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -495,6 +495,11 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons else output_path; + // Override WASM shell template from #run if set + if (comp.getBuildWasmShell()) |shell| { + merged_config.wasm_shell_path = shell; + } + // Ensure output directory exists if (std.mem.lastIndexOfScalar(u8, final_output, '/')) |sep| { if (sep > 0) { diff --git a/src/parser.zig b/src/parser.zig index 610dce6..132b2bf 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -2405,9 +2405,28 @@ pub const Parser = struct { try targets.append(self.allocator, target); } - // Only plain '=' is allowed + // Destructuring declaration: a, b := expr; + if (self.current.tag == .colon_equal) { + self.advance(); + // All targets must be plain identifiers + var names = std.ArrayList([]const u8).empty; + for (targets.items) |target| { + if (target.data != .identifier) { + return self.fail("destructuring targets must be identifiers"); + } + try names.append(self.allocator, target.data.identifier.name); + } + const value = try self.parseExpr(); + try self.expectSemicolonAfter(value); + return try self.createNode(start, .{ .destructure_decl = .{ + .names = try names.toOwnedSlice(self.allocator), + .value = value, + } }); + } + + // Multi-target assignment: only plain '=' is allowed if (self.current.tag != .equal) { - return self.fail("multi-target assignment requires '='"); + return self.fail("multi-target assignment requires '=' or ':='"); } self.advance(); diff --git a/src/sema.zig b/src/sema.zig index 0c36e9e..310cfd5 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -817,6 +817,9 @@ pub const Analyzer = struct { for (ma.targets) |t| try self.analyzeNode(t); for (ma.values) |v| try self.analyzeNode(v); }, + .destructure_decl => |dd| { + try self.analyzeNode(dd.value); + }, .return_stmt => |ret| { if (ret.value) |val| { try self.analyzeNode(val); @@ -1226,6 +1229,9 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { if (findNodeAtOffset(v, offset)) |found| return found; } }, + .destructure_decl => |dd| { + if (findNodeAtOffset(dd.value, offset)) |found| return found; + }, .return_stmt => |ret| { if (ret.value) |val| { if (findNodeAtOffset(val, offset)) |found| return found; diff --git a/src/target.zig b/src/target.zig index e51b15d..4774cf3 100644 --- a/src/target.zig +++ b/src/target.zig @@ -21,6 +21,8 @@ pub const TargetConfig = struct { sysroot: ?[]const u8 = null, /// Extra flags passed through to the linker (e.g. Emscripten -s flags). extra_link_flags: []const []const u8 = &.{}, + /// Custom WASM shell template path (overrides the built-in template). + wasm_shell_path: ?[]const u8 = null, pub const OptLevel = enum { none, @@ -192,12 +194,16 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex try argv.append(allocator, "-sMEMORY64"); } - // Use the built-in sx HTML shell template (write to temp file for emcc) + // HTML shell template: use custom path if set, otherwise write built-in template to temp file if (std.mem.endsWith(u8, output_bin, ".html")) { - const shell_html = @embedFile("wasm_shell.html"); - const shell_path = try std.fmt.allocPrint(allocator, "{s}.shell.html", .{output_obj}); - std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = shell_path, .data = shell_html }) catch {}; - try argv.appendSlice(allocator, &.{ "--shell-file", shell_path }); + if (target_config.wasm_shell_path) |custom_shell| { + try argv.appendSlice(allocator, &.{ "--shell-file", custom_shell }); + } else { + const shell_html = @embedFile("wasm_shell.html"); + const shell_path = try std.fmt.allocPrint(allocator, "{s}.shell.html", .{output_obj}); + std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = shell_path, .data = shell_html }) catch {}; + try argv.appendSlice(allocator, &.{ "--shell-file", shell_path }); + } } // Extra linker flags (e.g. -sUSE_SDL=3, -sUSE_WEBGL2=1, --preload-file assets) diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index fcfa3de..88340e9 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -128,6 +128,7 @@ slice-of-slice: [20, 30] strsub: world str-prefix: hello str-suffix: world +global-addr-of: 99 deref: Point{x: 10, y: 20} auto-deref: 10 mp[0]: 10 @@ -137,6 +138,7 @@ ptr==null: true ptr!=null: false ptr2==null: false ptr2!=null: true +ptr-nested-field: 1.000000 2.000000 3.000000 vec-construct: [1.000000, 3.000000, 2.000000] vec-add: [5.000000, 7.000000, 9.000000] vec-sub: [4.000000, 3.000000, 2.000000] @@ -267,6 +269,11 @@ flags-explicit-raw: 68 var swap: 20 10 arr swap: 3 1 3-way: 3 1 2 +--- destructure --- +basic: 10 20 +fn: 2 1 +discard: 200 +triple: 1 2 3 === 15. Foreign === foreign-rename: 42 === 16. Compound Assign ===