From 22bc2439ce2438d5b87f7ff2586bc2797d2ac6c3 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 4 Mar 2026 17:12:56 +0200 Subject: [PATCH] fixes --- examples/50-smoke.sx | 10 +++ examples/51-compound-assign-global.sx | 55 +++++++++++++++++ src/ir/emit_llvm.zig | 7 ++- src/ir/lower.zig | 88 +++++++++++++++++++++------ src/main.zig | 5 ++ src/target.zig | 68 +++++++++++++++++++++ tests/expected/50-smoke.txt | 2 + 7 files changed, 217 insertions(+), 18 deletions(-) create mode 100644 examples/51-compound-assign-global.sx diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index ffb022b..00496cf 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -769,6 +769,12 @@ END; ip3 := @out3.inner; print("ptr-nested-field: {} {} {}\n", ip3.a, ip3.b, ip3.c); + // Store to many-pointer field must not corrupt adjacent memory + MpHolder :: struct { items: [*]s64; sentinel: s64; } + mph := MpHolder.{ items = xx 0, sentinel = 42 }; + mph.items = xx 0; + print("mp-store-sentinel: {}\n", mph.sentinel); + // --- Vectors --- vc := vec3(1, 3, 2); print("vec-construct: {}\n", vc); @@ -3165,6 +3171,10 @@ END; print("wasm 64-bit??\n"); } } + + // POINTER_SIZE in regular (non-inline) if expression + ps := if POINTER_SIZE == 8 then "8" else "4"; + print("pointer size via if: {}\n", ps); } // ── Trailing commas ────────────────────────────────────────── diff --git a/examples/51-compound-assign-global.sx b/examples/51-compound-assign-global.sx new file mode 100644 index 0000000..4d7af78 --- /dev/null +++ b/examples/51-compound-assign-global.sx @@ -0,0 +1,55 @@ +// Test: compound assignment operators on global variables +// Ensures += -= *= /= %= &= |= ^= <<= >>= all do load-modify-store + +#import "modules/std.sx"; + +g_add : s64 = 10; +g_sub : s64 = 10; +g_mul : s64 = 10; +g_div : s64 = 100; +g_mod : s64 = 10; +g_and : s64 = 0xff; +g_or : s64 = 0x0f; +g_xor : s64 = 0xff; +g_shl : s64 = 1; +g_shr : s64 = 256; + +main :: () -> void { + // += repeated: should accumulate, not reset + g_add += 1; + g_add += 1; + g_add += 1; + print("add: {}\n", g_add); // 13 + + g_sub -= 3; + g_sub -= 2; + print("sub: {}\n", g_sub); // 5 + + g_mul *= 2; + g_mul *= 3; + print("mul: {}\n", g_mul); // 60 + + g_div /= 5; + g_div /= 4; + print("div: {}\n", g_div); // 5 + + g_mod %= 3; + print("mod: {}\n", g_mod); // 1 + + g_and &= 0x0f; + print("and: {}\n", g_and); // 15 + + g_or |= 0xf0; + print("or: {}\n", g_or); // 255 + + g_xor ^= 0x0f; + print("xor: {}\n", g_xor); // 240 + + g_shl <<= 4; + g_shl <<= 2; + print("shl: {}\n", g_shl); // 64 + + g_shr >>= 3; + g_shr >>= 2; + print("shr: {}\n", g_shr); // 8 +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 8ff5cc8..ed28856 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -706,11 +706,15 @@ pub const LLVMEmitter = struct { if (ptr_kind == c.LLVMPointerTypeKind and val_kind != c.LLVMVoidTypeKind) { // Coerce value to match the IR-declared pointer target type. // E.g. storing i64 to *i8 (from index_gep on string) needs truncation. + // + // Only unwrap .pointer (from index_gep/alloca: *element → element). + // Never unwrap .many_pointer — it only appears as struct_gep field + // value types (e.g., [*]BigNode), where unwrapping to the element + // type gives a wrong store size (stores BigNode-sized instead of ptr). if (self.getRefIRType(st.ptr)) |ptr_ir_ty| { const pointee_info = self.ir_mod.types.get(ptr_ir_ty); const target_ty: ?c.LLVMTypeRef = switch (pointee_info) { .pointer => |p| self.toLLVMType(p.pointee), - .many_pointer => |p| self.toLLVMType(p.element), else => null, }; if (target_ty) |tt| { @@ -2566,6 +2570,7 @@ pub const LLVMEmitter = struct { return null; } + /// Coerce both binary operands to match the instruction's result type. /// E.g. if result is i64 but one operand is i32, sext it. fn matchBinOpTypes(self: *LLVMEmitter, lhs: *c.LLVMValueRef, rhs: *c.LLVMValueRef, result_ty: TypeId) void { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index e2e8ed3..c3e3a09 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1143,12 +1143,19 @@ pub const Lowering = struct { // Fallback: global variable assignment if (!handled) { if (self.global_names.get(id.name)) |gi| { - const val_ty = self.builder.getRefType(val); - const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) - self.coerceToType(val, val_ty, gi.ty) - else - val; - self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void); + if (asgn.op == .assign) { + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) + self.coerceToType(val, val_ty, gi.ty) + else + val; + self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void); + } else { + // Compound assignment: load current value, apply op, store back + const loaded = self.builder.emit(.{ .global_get = gi.id }, gi.ty); + const result = self.emitCompoundOp(loaded, val, asgn.op, gi.ty); + self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = result } }, .void); + } } } }, @@ -1228,7 +1235,12 @@ pub const Lowering = struct { break; } } - const gep = self.builder.structGepTyped(obj_ptr, field_idx, field_ty, obj_ty); + // Wrap in ptrTo so the store handler sees *field_ty (consistent + // with index_gep which uses ptrTo(elem_ty)). Without this, a + // [*]BigNode field makes the store handler extract BigNode as the + // 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); const coerced = self.coerceToType(val, src_ty, field_ty); @@ -1434,6 +1446,13 @@ pub const Lowering = struct { break :blk binding.ref; } } + // Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals + if (self.comptime_constants.get(id.name)) |cv| { + switch (cv) { + .int_val => |iv| break :blk self.builder.constInt(iv, .s64), + .enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty), + } + } // Check globals (#run constants) if (self.global_names.get(id.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); @@ -2672,7 +2691,7 @@ pub const Lowering = struct { for (lowered.items) |l| { if (std.mem.eql(u8, l.name, sf_name)) { var val = l.val; - const src_ty = self.inferExprType(l.node); + const src_ty = self.builder.getRefType(val); val = self.coerceToType(val, src_ty, sf.ty); fields.append(self.alloc, val) catch unreachable; found = true; @@ -3548,10 +3567,32 @@ pub const Lowering = struct { } } - // Handle closure(lambda) — just return the lambda's closure_create result + // Handle closure(fn_or_lambda) — wrap bare functions into closures if (c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "closure")) { if (c.args.len >= 1) { - return self.lowerExpr(c.args[0]); + const arg = c.args[0]; + // If argument is a bare function name, create a proper closure from it + if (arg.data == .identifier) { + const fn_name = arg.data.identifier.name; + if (!self.lowered_functions.contains(fn_name)) { + self.lazyLowerFunction(fn_name); + } + if (self.resolveFuncByName(fn_name)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + // Build closure type from function signature + var param_types_list = std.ArrayList(TypeId).empty; + defer param_types_list.deinit(self.alloc); + for (func.params) |p| { + param_types_list.append(self.alloc, p.ty) catch unreachable; + } + const closure_ty = self.module.types.closureType(param_types_list.items, func.ret); + const closure_info = self.module.types.get(closure_ty).closure; + const tramp_id = self.createBareFnTrampoline(fid, closure_info); + return self.builder.closureCreate(tramp_id, Ref.none, closure_ty); + } + } + // Lambda or other expression — already produces closure_create + return self.lowerExpr(arg); } } @@ -6343,18 +6384,31 @@ pub const Lowering = struct { /// Check if a match expression is a type-category match (patterns are type/category names). fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { - // Infer result type from the first non-default arm body + // Infer result type from the first non-null arm body. + // If we skip null_literal arms and find a concrete type T, and there + // were null arms, the result is ?T (optional). + var has_null = false; for (me.arms) |arm| { - if (arm.body.data == .block) { - // Block — check last statement + const last_node = if (arm.body.data == .block) blk: { if (arm.body.data.block.stmts.len > 0) { - const last = arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1]; - return self.inferExprType(last); + break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1]; } + break :blk arm.body; + } else arm.body; + + if (last_node.data == .null_literal) { + has_null = true; + continue; } - return self.inferExprType(arm.body); + + // First non-null arm determines the type (same as old behavior) + const arm_ty = self.inferExprType(last_node); + if (has_null and arm_ty != .void) { + return self.module.types.optionalOf(arm_ty); + } + return arm_ty; } - return .s64; + return .void; } fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { diff --git a/src/main.zig b/src/main.zig index e859250..6fed0a6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -515,6 +515,11 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons }; timer.record("link"); + // Post-process wasm HTML: inject content hash for cache busting + if (merged_config.isEmscripten() and std.mem.endsWith(u8, final_output, ".html")) { + sx.target.postProcessWasmHtml(allocator, io, final_output); + } + // Save linked binary to cache if (enable_cache) { std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {}; diff --git a/src/target.zig b/src/target.zig index 4774cf3..253386c 100644 --- a/src/target.zig +++ b/src/target.zig @@ -270,6 +270,74 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex if (result.exited != 0) return error.LinkError; } +/// After emcc produces HTML output, inject cache-busting hashes into the +/// generated + // Inject ?v=HASH into the src and prepend a Module.locateFile script. + while (std.mem.indexOfPos(u8, html, pos, "src=\"")) |src_start| { + const val_start = src_start + 5; // past src=" + const val_end = std.mem.indexOfPos(u8, html, val_start, "\"") orelse break; + const src_val = html[val_start..val_end]; + + if (!std.mem.endsWith(u8, src_val, ".js")) { + // Not a .js src — skip past this attribute and keep searching + pos = val_end + 1; + continue; + } + + // Find the opening < of this tag to inject locateFile before it + const tag_start = if (std.mem.lastIndexOf(u8, html[pos..src_start], "<")) |off| pos + off else src_start; + + // Copy everything up to the tag start + out.appendSlice(allocator, html[pos..tag_start]) catch return; + + // Inject Module.locateFile once, before the first .js script tag + if (!injected_locateFile) { + out.appendSlice(allocator, "\n") catch return; + injected_locateFile = true; + } + + // Copy tag up to the closing quote of src, inserting ?v=HASH + out.appendSlice(allocator, html[tag_start..val_end]) catch return; + out.appendSlice(allocator, "?v=") catch return; + out.appendSlice(allocator, hash_hex) catch return; + + pos = val_end; + } + // Copy remaining HTML + out.appendSlice(allocator, html[pos..]) catch return; + + const final = out.toOwnedSlice(allocator) catch return; + std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = html_path, .data = final }) catch {}; +} + /// Common library paths for the host OS, computed at comptime. pub const host_lib_paths = blk: { const builtin = @import("builtin"); diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 88340e9..b5c3c93 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -139,6 +139,7 @@ ptr!=null: false ptr2==null: false ptr2!=null: true ptr-nested-field: 1.000000 2.000000 3.000000 +mp-store-sentinel: 42 vec-construct: [1.000000, 3.000000, 2.000000] vec-add: [5.000000, 7.000000, 9.000000] vec-sub: [4.000000, 3.000000, 2.000000] @@ -609,6 +610,7 @@ usize->s64: 42 not wasm known os desktop 64-bit +pointer size via if: 8 === Trailing Commas === trailing commas ok === DONE ===