This commit is contained in:
agra
2026-03-04 17:12:56 +02:00
parent 67e02a20a5
commit 22bc2439ce
7 changed files with 217 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <script> tag and add Module.locateFile for .wasm/.data files.
pub fn postProcessWasmHtml(allocator: std.mem.Allocator, io: std.Io, html_path: []const u8) void {
const base = if (std.mem.endsWith(u8, html_path, ".html"))
html_path[0 .. html_path.len - 5]
else
return;
// Hash build output contents (.js + .wasm + optional .data)
var hash: u64 = 0;
const exts = [_][]const u8{ ".js", ".wasm", ".data" };
for (exts) |ext| {
const path = std.fmt.allocPrint(allocator, "{s}{s}", .{ base, ext }) catch continue;
if (std.Io.Dir.readFileAlloc(.cwd(), io, path, allocator, .limited(64 * 1024 * 1024))) |data| {
hash = std.hash.Wyhash.hash(hash, data);
} else |_| {}
}
const hash_hex = std.fmt.allocPrint(allocator, "{x:0>8}", .{@as(u32, @truncate(hash))}) catch return;
// Read the final HTML produced by emcc
const html = std.Io.Dir.readFileAlloc(.cwd(), io, html_path, allocator, .limited(10 * 1024 * 1024)) catch return;
var out = std.ArrayList(u8).empty;
var pos: usize = 0;
var injected_locateFile = false;
// Find emcc's generated script tag: <script ...src="*.js"></script>
// 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, "<script>Module.locateFile=function(p){return p+'?v=") catch return;
out.appendSlice(allocator, hash_hex) catch return;
out.appendSlice(allocator, "'}</script>\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");

View File

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