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