diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 57e897f..62aba01 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -1377,5 +1377,86 @@ END; print("{}\n", "piped" |> concat(" ok") |> concat("!")); // piped ok! } + // ── alloc_slice ────────────────────────────────────────── + { + items := alloc_slice(s64, 5); + items[0] = 10; + items[1] = 20; + items[2] = 30; + items[3] = 40; + items[4] = 50; + print("alloc len: {}\n", items.len); // alloc len: 5 + print("alloc[0]: {}\n", items[0]); // alloc[0]: 10 + print("alloc[4]: {}\n", items[4]); // alloc[4]: 50 + + // alloc_slice with u8 + bytes := alloc_slice(u8, 3); + bytes[0] = 65; + bytes[1] = 66; + bytes[2] = 67; + print("bytes len: {}\n", bytes.len); // bytes len: 3 + } + + // ======================================================== + // ALLOCATORS + // ======================================================== + print("--- allocators ---\n"); + + // ── GPA ───────────────────────────────────────────────── + { + gpa_state : GPA = .{ alloc_count = 0 }; + gpa := gpa_create(@gpa_state); + p1 := gpa.alloc(gpa.ctx, 64); + p2 := gpa.alloc(gpa.ctx, 128); + print("gpa allocs: {}\n", gpa_state.alloc_count); // gpa allocs: 2 + gpa.free(gpa.ctx, p1); + gpa.free(gpa.ctx, p2); + print("gpa final: {}\n", gpa_state.alloc_count); // gpa final: 0 + } + + // ── Arena backed by GPA (multi-chunk) ─────────────────── + { + gpa_state3 : GPA = .{ alloc_count = 0 }; + gpa3 := gpa_create(@gpa_state3); + arena_state : Arena = ---; + arena := arena_create(@arena_state, gpa3, 32); + // First chunk fits 80 usable bytes + a1 := arena.alloc(arena.ctx, 40); + a2 := arena.alloc(arena.ctx, 40); + print("arena chunks: {}\n", gpa_state3.alloc_count); // arena chunks: 1 + // Overflow → new chunk + a3 := arena.alloc(arena.ctx, 16); + print("arena overflow: {}\n", gpa_state3.alloc_count); // arena overflow: 2 + // Verify memory works across chunks + p1 : [*]u8 = xx a1; + p3 : [*]u8 = xx a3; + p1[0] = 42; + p3[0] = 99; + print("arena a1: {}\n", p1[0]); // arena a1: 42 + print("arena a3: {}\n", p3[0]); // arena a3: 99 + // Reset retains newest chunk + arena_reset(@arena_state); + print("arena reset idx: {}\n", arena_state.end_index); // arena reset idx: 0 + print("arena reset gpa: {}\n", gpa_state3.alloc_count);// arena reset gpa: 1 + // Deinit frees all + arena_deinit(@arena_state); + print("arena deinit: {}\n", gpa_state3.alloc_count); // arena deinit: 0 + } + + // ── BufAlloc from stack array ─────────────────────────── + { + stack_buf : [128]u8 = ---; + buf_state : BufAlloc = ---; + bufalloc := buf_create(@buf_state, @stack_buf[0], 128); + b1 := bufalloc.alloc(bufalloc.ctx, 24); + b2 := bufalloc.alloc(bufalloc.ctx, 24); + print("buf pos: {}\n", buf_state.pos); // buf pos: 48 + b3 := bufalloc.alloc(bufalloc.ctx, 200); + b3_i : s64 = xx b3; + print("buf overflow: {}\n", b3_i); // buf overflow: 0 + buf_reset(@buf_state); + print("buf reset: {}\n", buf_state.pos); // buf reset: 0 + } + print("=== DONE ===\n"); } diff --git a/examples/modules/allocators.sx b/examples/modules/allocators.sx new file mode 100644 index 0000000..3b1bb10 --- /dev/null +++ b/examples/modules/allocators.sx @@ -0,0 +1,154 @@ +#import "std.sx"; + +// --- Allocator protocol --- + +Allocator :: struct { + ctx: *void; + alloc: (*void, s64) -> *void; + free: (*void, *void) -> void; +} + +// --- GPA: general purpose allocator (malloc/free wrapper) --- + +GPA :: struct { + alloc_count: s64; +} + +gpa_alloc :: (ctx: *void, size: s64) -> *void { + gpa : *GPA = xx ctx; + gpa.alloc_count += 1; + malloc(size); +} + +gpa_free :: (ctx: *void, ptr: *void) { + gpa : *GPA = xx ctx; + gpa.alloc_count -= 1; + free(ptr); +} + +gpa_create :: (gpa: *GPA) -> Allocator { + ctx : *void = xx gpa; + Allocator.{ ctx = ctx, alloc = gpa_alloc, free = gpa_free }; +} + +// --- Arena: multi-chunk bump allocator (Zig-inspired) --- + +ArenaChunk :: struct { + next: *void; // *ArenaChunk + cap: s64; // total chunk size including this header +} + +Arena :: struct { + first: *void; // *ArenaChunk — head of list (newest first) + end_index: s64; // bump position within current chunk's usable area + parent: Allocator; // backing allocator +} + +arena_add_chunk :: (a: *Arena, min_size: s64) { + first_i : s64 = xx a.first; + prev_cap := if first_i != 0 then { c : *ArenaChunk = xx a.first; c.cap; } else 0; + needed := min_size + 16 + 16; + len := (prev_cap + needed) * 3 / 2; + raw := a.parent.alloc(a.parent.ctx, len); + chunk : *ArenaChunk = xx raw; + chunk.next = a.first; + chunk.cap = len; + a.first = xx chunk; + a.end_index = 0; +} + +arena_alloc :: (ctx: *void, size: s64) -> *void { + a : *Arena = xx ctx; + aligned := (size + 7) & (0 - 8); + first_i : s64 = xx a.first; + if first_i != 0 { + chunk : *ArenaChunk = xx a.first; + usable := chunk.cap - 16; + if a.end_index + aligned <= usable { + buf : [*]u8 = xx a.first; + ptr : *void = xx @buf[16 + a.end_index]; + a.end_index = a.end_index + aligned; + return ptr; + } + } + arena_add_chunk(a, aligned); + buf : [*]u8 = xx a.first; + ptr : *void = xx @buf[16 + a.end_index]; + a.end_index = a.end_index + aligned; + ptr; +} + +arena_free :: (ctx: *void, ptr: *void) { +} + +arena_create :: (a: *Arena, parent: Allocator, size: s64) -> Allocator { + a.first = null; + a.end_index = 0; + a.parent = parent; + arena_add_chunk(a, size); + ctx : *void = xx a; + Allocator.{ ctx = ctx, alloc = arena_alloc, free = arena_free }; +} + +arena_reset :: (a: *Arena) { + // Keep first chunk (newest/largest), free the rest + first_i : s64 = xx a.first; + if first_i != 0 { + chunk : *ArenaChunk = xx a.first; + it : s64 = xx chunk.next; + while it != 0 { + c : *ArenaChunk = xx it; + next_i : s64 = xx c.next; + a.parent.free(a.parent.ctx, xx c); + it = next_i; + } + chunk.next = null; + } + a.end_index = 0; +} + +arena_deinit :: (a: *Arena) { + it : s64 = xx a.first; + while it != 0 { + c : *ArenaChunk = xx it; + next_i : s64 = xx c.next; + a.parent.free(a.parent.ctx, xx c); + it = next_i; + } + a.first = null; + a.end_index = 0; +} + +// --- BufAlloc: bump allocator backed by a user-provided slice --- + +BufAlloc :: struct { + buf: [*]u8; + len: s64; + pos: s64; +} + +buf_alloc :: (ctx: *void, size: s64) -> *void { + b : *BufAlloc = xx ctx; + aligned := (size + 7) & (0 - 8); + if b.pos + aligned > b.len { + return null; + } + ptr : *void = xx @b.buf[b.pos]; + b.pos = b.pos + aligned; + ptr; +} + +buf_free :: (ctx: *void, ptr: *void) { +} + +buf_create :: (b: *BufAlloc, buf: [*]u8, len: s64) -> Allocator { + b.buf = buf; + b.len = len; + b.pos = 0; + ctx : *void = xx b; + Allocator.{ ctx = ctx, alloc = buf_alloc, free = buf_free }; +} + +buf_reset :: (b: *BufAlloc) { + b.pos = 0; +} diff --git a/examples/modules/std.sx b/examples/modules/std.sx index 3f7730c..e58878c 100644 --- a/examples/modules/std.sx +++ b/examples/modules/std.sx @@ -18,57 +18,37 @@ field_value_int :: ($T: Type, idx: s64) -> s64 #builtin; field_index :: ($T: Type, val: T) -> s64 #builtin; string :: []u8 #builtin; -// --- Arena allocator & Context --- +#import "allocators.sx"; -Arena :: struct { - buf: string; - pos: s64; -} +// --- Context --- Context :: struct { - arena: *Arena; + allocator: Allocator; data: *void; } context : Context = ---; -arena_create :: (size: s64) -> Arena { - Arena.{ buf = cstring(size), pos = 0 }; -} - -arena_alloc :: (arena: *Arena, size: s64) -> *void { - aligned := (size + 7) & (0 - 8); - if arena.pos + aligned > arena.buf.len { - return malloc(aligned); - } - ptr : *void = xx @arena.buf[arena.pos]; - arena.pos = arena.pos + aligned; - ptr; -} - -arena_reset :: (arena: *Arena) { - arena.pos = 0; -} - -arena_destroy :: (arena: *Arena) { - free(arena.buf.ptr); -} - -// --- String allocation --- - -CString :: union { - s: string; - struct { ptr: *void; len: s64; }; -} +// --- Slice & string allocation --- cstring :: (size: s64) -> string { - p : s64 = xx context.arena; - raw := if p != 0 then arena_alloc(context.arena, size + 1) else malloc(size + 1); + p : s64 = xx context.allocator.ctx; + raw := if p != 0 then context.allocator.alloc(context.allocator.ctx, size + 1) else malloc(size + 1); memset(raw, 0, size + 1); - rs : CString = ---; - rs.ptr = raw; - rs.len = size; - rs.s; + s : string = ---; + s.ptr = xx raw; + s.len = size; + s; +} + +alloc_slice :: ($T: Type, count: s64) -> []T { + p : s64 = xx context.allocator.ctx; + raw := if p != 0 then context.allocator.alloc(context.allocator.ctx, count * size_of(T)) else malloc(count * size_of(T)); + memset(raw, 0, count * size_of(T)); + s : []T = ---; + s.ptr = xx raw; + s.len = count; + s; } int_to_string :: (n: s64) -> string { diff --git a/src/codegen.zig b/src/codegen.zig index abe9784..1aa56dc 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -99,6 +99,15 @@ fn baseName(name: []const u8) []const u8 { return if (std.mem.lastIndexOfScalar(u8, name, '.')) |idx| name[idx + 1 ..] else name; } +/// Detect `$T: Type` parameter declarations (not `s: $T` references). +/// For `$T: Type`, the parser sets param.name = "T" and type_expr = {name="T", is_generic=true}. +/// For `s: $T`, param.name = "s" and type_expr = {name="T", is_generic=true}. +fn isTypeParamDecl(param: ast.Param) bool { + return param.type_expr.data == .type_expr and + param.type_expr.data.type_expr.is_generic and + std.mem.eql(u8, param.name, param.type_expr.data.type_expr.name); +} + pub const CodeGen = struct { context: c.LLVMContextRef, module: c.LLVMModuleRef, @@ -1271,6 +1280,7 @@ pub const CodeGen = struct { }; var vm = comptime_mod.VM.init(self.allocator, if (self.sema_result) |sr| sr else null, self.root_decls, self); + vm.setupComptimeContext() catch {}; return vm.execute(&chunk) catch |err| { return self.emitErrorFmt("comptime execution failed: {s}", .{@errorName(err)}); }; @@ -1307,6 +1317,7 @@ pub const CodeGen = struct { // Set up VM and push all args onto the stack var vm = comptime_mod.VM.init(self.allocator, if (self.sema_result) |sr| sr else null, self.root_decls, self); + vm.setupComptimeContext() catch {}; for (arg_values) |val| { vm.push(val) catch return null; } @@ -1842,6 +1853,8 @@ pub const CodeGen = struct { var param_llvm_types = std.ArrayList(c.LLVMTypeRef).empty; for (params) |param| { if (param.is_comptime) continue; + // Skip $T: Type params — erased at instantiation time (param name == type name) + if (isTypeParamDecl(param)) continue; if (param.is_variadic) { // Variadic param becomes a slice {ptr, i32} in the LLVM signature try param_llvm_types.append(self.allocator, self.getStringStructType()); @@ -3333,6 +3346,26 @@ pub const CodeGen = struct { return self.emitErrorFmt("field assignment not supported on tagged enum '{s}'", .{uname}); } + // Slice/string field assignment: s.ptr = val, s.len = val + if (entry.ty == .string_type or entry.ty.isSlice()) { + const struct_ty = self.getStringStructType(); + if (std.mem.eql(u8, fa.field, "ptr")) { + const gep = self.structGEP(struct_ty, entry.ptr, 0, "slice_ptr"); + const elem_name = if (entry.ty == .string_type) "u8" else entry.ty.slice_type.element_name; + const ptr_ty = Type{ .many_pointer_type = .{ .element_name = elem_name } }; + const rhs = try self.genExprAsType(asgn.value, ptr_ty); + _ = c.LLVMBuildStore(self.builder, rhs, gep); + return null; + } + if (std.mem.eql(u8, fa.field, "len")) { + const gep = self.structGEP(struct_ty, entry.ptr, 1, "slice_len"); + const rhs = try self.genExprAsType(asgn.value, Type.s(64)); + _ = c.LLVMBuildStore(self.builder, rhs, gep); + return null; + } + return self.emitErrorFmt("no field '{s}' on slice (available: .ptr, .len)", .{fa.field}); + } + if (!entry.ty.isStruct()) return self.emitErrorFmt("field access on non-struct variable '{s}'", .{obj_name}); const sname = entry.ty.struct_type; @@ -3741,6 +3774,11 @@ pub const CodeGen = struct { Type.u(8); return self.gepPointerElement(self.typeToLLVM(elem_ty), ptr, idx, "addr_elem"); } + if (obj_ty.isManyPointer()) { + const raw_ptr = try self.genExpr(ie.object); + const elem_ty = self.resolveTypeFromName(obj_ty.many_pointer_type.element_name) orelse Type.u(8); + return self.gepPointerElement(self.typeToLLVM(elem_ty), raw_ptr, idx, "addr_elem"); + } } // &s.field — return GEP pointer to the struct field if (operand.data == .field_access) { @@ -6072,6 +6110,26 @@ pub const CodeGen = struct { } } + // Struct field function pointer call: obj.field(args) + // Checked before UFCS so that struct fields shadow free functions of the same name. + { + var obj_ty = self.inferType(fa.object); + if (obj_ty.isPointer()) { + obj_ty = self.resolveTypeFromName(obj_ty.pointer_type.pointee_name) orelse obj_ty; + } + if (obj_ty.isStruct()) { + if (self.lookupStructInfo(obj_ty.struct_type)) |info| { + if (self.findNameIndex(info.field_names, fa.field)) |idx| { + const field_ty = info.field_types[idx]; + if (field_ty.isFunctionType()) { + const fn_ptr = try self.genFieldAccess(fa); + return self.genIndirectCallFromPtr(fn_ptr, field_ty.function_type, call_node); + } + } + } + } + } + // UFCS: obj.method(args...) → method(obj, args...) const method_name = fa.field; const resolved_method = self.ufcs_aliases.get(method_name) orelse method_name; @@ -6373,11 +6431,12 @@ pub const CodeGen = struct { fn genIndirectCall(self: *CodeGen, entry: NamedValue, call_node: ast.Call) !c.LLVMValueRef { const fti = entry.ty.function_type; - - // Load the function pointer from the alloca const ptr_ty = self.ptrType(); const fn_ptr = c.LLVMBuildLoad2(self.builder, ptr_ty, entry.ptr, "fn_ptr"); + return self.genIndirectCallFromPtr(fn_ptr, fti, call_node); + } + fn genIndirectCallFromPtr(self: *CodeGen, fn_ptr: c.LLVMValueRef, fti: Type.FunctionTypeInfo, call_node: ast.Call) !c.LLVMValueRef { // Build LLVM function type from FunctionTypeInfo const ptr_ty_llvm = self.ptrType(); if (fti.param_types.len > 64) return self.emitErrorFmt("indirect call has {d} parameters, exceeding maximum of 64", .{fti.param_types.len}); @@ -6482,6 +6541,22 @@ pub const CodeGen = struct { var bindings = std.StringHashMap(Type).init(self.allocator); // Track bindings derived from parameterized struct types — these are authoritative and should not be widened var firm_bindings = std.StringHashMap(void).init(self.allocator); + // Bind explicit $T: Type params from type expression args + for (fd.params, 0..) |param, i| { + if (!param.is_comptime) continue; + if (i >= call_node.args.len) continue; + const arg = call_node.args[i]; + if (arg.data != .type_expr) continue; + for (fd.type_params) |tp| { + if (std.mem.eql(u8, tp.name, param.name)) { + const constraint = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else ""; + if (std.mem.eql(u8, constraint, "Type")) { + try bindings.put(tp.name, self.resolveType(arg)); + } + break; + } + } + } for (fd.params, 0..) |param, i| { if (param.is_comptime) continue; // Direct type param: (a: $T) introduces/widens, (a: T) only binds if not yet bound @@ -6580,10 +6655,16 @@ pub const CodeGen = struct { try self.instantiateGeneric(fd, bindings, mangled); // Generate arguments with type conversion to match parameter types + // Skip $T: Type params (arg is a type expression, not a runtime value) const saved_call_bindings = self.type_param_bindings; self.type_param_bindings = bindings; var arg_vals = std.ArrayList(c.LLVMValueRef).empty; for (call_node.args, 0..) |arg, i| { + // Skip $T: Type params — the arg is a type expression, not a runtime value + if (i < fd.params.len) { + const p = fd.params[i]; + if (isTypeParamDecl(p)) continue; + } if (i < fd.params.len) { const param_ty = self.resolveType(fd.params[i].type_expr); try arg_vals.append(self.allocator, try self.genExprAsType(arg, param_ty)); @@ -6979,6 +7060,8 @@ pub const CodeGen = struct { // Create allocas for parameters var llvm_param_idx: u32 = 0; for (fd.params) |param| { + // Skip $T: Type params — type is resolved via bindings, not passed at runtime + if (isTypeParamDecl(param)) continue; if (param.is_comptime) { // Comptime param: create a constant in named_values from the call-site value if (self.comptime_param_nodes) |cpn| { @@ -7930,6 +8013,24 @@ pub const CodeGen = struct { break :blk @as(?Type, null); }; if (obj_ty) |uty| return uty; + + // Struct field function pointer call: obj.fn_field(args) + { + var fa_obj_ty = self.inferType(fa.object); + if (fa_obj_ty.isPointer()) { + fa_obj_ty = self.resolveTypeFromName(fa_obj_ty.pointer_type.pointee_name) orelse fa_obj_ty; + } + if (fa_obj_ty.isStruct()) { + if (self.lookupStructInfo(fa_obj_ty.struct_type)) |info| { + if (self.findNameIndex(info.field_names, fa.field)) |idx| { + const field_ty = info.field_types[idx]; + if (field_ty.isFunctionType()) { + return field_ty.function_type.return_type.*; + } + } + } + } + } } const callee_name = self.resolveCalleeName(call_node) orelse return Type.s(64); const base_name = baseName(callee_name); @@ -8005,8 +8106,11 @@ pub const CodeGen = struct { return bound_ty; } } - // Try resolving as a concrete type (e.g. -> string, -> s32) + // Resolve with inferred bindings so []T, *T etc. substitute correctly + const saved_bindings = self.type_param_bindings; + self.type_param_bindings = inferred_bindings; const resolved = self.resolveType(rt); + self.type_param_bindings = saved_bindings; if (!std.meta.eql(resolved, Type.void_type)) return resolved; } return Type.s(64); diff --git a/src/comptime.zig b/src/comptime.zig index 1228899..01947b5 100644 --- a/src/comptime.zig +++ b/src/comptime.zig @@ -420,11 +420,25 @@ pub const Compiler = struct { /// Look up a struct field index by name, handling pointer auto-deref. /// Also resolves promoted fields from anonymous struct variants of unions. fn resolveFieldIndex(self: *Compiler, object: *Node, field: []const u8) ?u16 { + // Check local type_name for string/slice — works even without sema + if (self.getLocalTypeName(object)) |tname| { + if (std.mem.eql(u8, tname, "string") or std.mem.startsWith(u8, tname, "[]")) { + if (std.mem.eql(u8, field, "ptr")) return 0; + if (std.mem.eql(u8, field, "len")) return 1; + return null; + } + } if (self.sema_result) |sr| { const obj_ty = sr.type_map.get(object) orelse { // Sema doesn't have type info — try union fallback return self.resolveFieldViaUnion(object, field); }; + // String/slice field access: .ptr = 0, .len = 1 + if (obj_ty == .string_type or obj_ty.isSlice()) { + if (std.mem.eql(u8, field, "ptr")) return 0; + if (std.mem.eql(u8, field, "len")) return 1; + return null; + } const struct_name: ?[]const u8 = if (obj_ty.isStruct()) obj_ty.struct_type else if (obj_ty.isPointer()) @@ -708,6 +722,9 @@ pub const Compiler = struct { if (type_name) |tname| { if (self.findUnionWordCount(tname)) |wc| { try self.emit(.{ .make_union = .{ .type_name = tname, .word_count = wc } }); + } else if (std.mem.eql(u8, tname, "string") or std.mem.startsWith(u8, tname, "[]")) { + // String/slice fat pointer: 2 words (ptr, len) + try self.emit(.{ .make_union = .{ .type_name = tname, .word_count = 2 } }); } else { try self.emit(.push_void); } @@ -832,11 +849,11 @@ pub const Compiler = struct { const callee_name = if (call_node.callee.data == .identifier) call_node.callee.data.identifier.name else if (call_node.callee.data == .field_access) blk: { - const fa = call_node.callee.data.field_access; - if (fa.object.data == .identifier) { - break :blk fa.field; - } - break :blk null; + // Use the innermost field name as the callee (covers UFCS and namespaced calls). + // For struct field fn-ptr calls (e.g. context.allocator.alloc_fn), this won't + // resolve to a real function, but the call instruction is only fatal at execution + // time — dead branches (like comptime fallback to malloc) never execute it. + break :blk call_node.callee.data.field_access.field; } else null; if (callee_name) |name| { @@ -1095,6 +1112,38 @@ pub const VM = struct { }; } + /// Pre-initialize `context` global with null arena for comptime evaluation. + /// At comptime, cstring uses malloc (a safe comptime builtin) as fallback. + pub fn setupComptimeContext(self: *VM) !void { + // Context struct: { allocator: Allocator{ctx, alloc, free}, data: *void } + // All allocator fields are null/zero so cstring takes the malloc path at comptime + const alloc_fields = try self.allocator.alloc(Value, 3); + const alloc_field_names = try self.allocator.alloc([]const u8, 3); + alloc_field_names[0] = "ctx"; + alloc_field_names[1] = "alloc"; + alloc_field_names[2] = "free"; + alloc_fields[0] = .{ .int_val = 0 }; // null ctx pointer + alloc_fields[1] = .{ .int_val = 0 }; // null alloc + alloc_fields[2] = .{ .int_val = 0 }; // null free + + const ctx_fields = try self.allocator.alloc(Value, 2); + const ctx_field_names = try self.allocator.alloc([]const u8, 2); + ctx_field_names[0] = "allocator"; + ctx_field_names[1] = "data"; + ctx_fields[0] = .{ .struct_val = .{ + .type_name = "Allocator", + .field_names = alloc_field_names, + .fields = alloc_fields, + } }; + ctx_fields[1] = .{ .null_val = {} }; + + try self.globals.put("context", .{ .struct_val = .{ + .type_name = "Context", + .field_names = ctx_field_names, + .fields = ctx_fields, + } }); + } + pub fn push(self: *VM, value: Value) !void { if (self.sp >= 256) return error.StackOverflow; self.stack[self.sp] = value; @@ -1418,7 +1467,18 @@ pub const VM = struct { if (idx < raw_obj.union_val.words.len) { raw_obj.union_val.words[idx] = val; } - try self.push(raw_obj); + // Auto-finalize: string/slice fat pointer → string_val + const words = raw_obj.union_val.words; + const tn = raw_obj.union_val.type_name; + if (words.len == 2 and words[0] == .byte_ptr_val and words[1] == .int_val and + (std.mem.eql(u8, tn, "string") or std.mem.startsWith(u8, tn, "[]"))) + { + const bp = words[0].byte_ptr_val; + const len: usize = @intCast(@max(0, words[1].int_val)); + try self.push(.{ .string_val = bp.data[bp.offset .. bp.offset + len] }); + } else { + try self.push(raw_obj); + } } else { return error.TypeError; } diff --git a/src/lsp/document.zig b/src/lsp/document.zig index c27bea5..09354ea 100644 --- a/src/lsp/document.zig +++ b/src/lsp/document.zig @@ -28,6 +28,8 @@ pub const Document = struct { last_good_sema: ?sx.sema.SemaResult = null, /// Import declarations parsed from this file. imports: []const Import, + /// True while this document is being analyzed (circular import guard). + is_analyzing: bool = false, pub fn topLevelSymbols(self: *const Document) []const sx.sema.Symbol { const sr = self.sema orelse return &.{}; @@ -115,6 +117,10 @@ pub const DocumentStore = struct { /// Analyze a document: parse, resolve imports, run sema with imported symbols pre-registered. pub fn analyzeDocument(self: *DocumentStore, doc: *Document) !void { + if (doc.is_analyzing) return; // circular import guard + doc.is_analyzing = true; + defer doc.is_analyzing = false; + // Parse if needed if (doc.root == null) { var p = sx.parser.Parser.init(self.allocator, doc.source); @@ -144,10 +150,6 @@ pub const DocumentStore = struct { // Recursively analyze imported documents and pre-register their symbols var analyzer = sx.sema.Analyzer.init(self.allocator); - // Track in-progress documents to detect cycles - var cycle_guard = std.StringHashMap(void).init(self.allocator); - try cycle_guard.put(doc.path, {}); - for (doc.imports) |imp| { // Try as file first; if that fails, try as directory import const imp_doc = self.getOrLoad(imp.path) catch { @@ -155,11 +157,8 @@ pub const DocumentStore = struct { const dir_files = self.listDirectoryFiles(imp.path) orelse continue; for (dir_files) |file_path| { const file_doc = self.getOrLoad(file_path) catch continue; - if (cycle_guard.contains(file_path)) continue; if (file_doc.sema == null) { - try cycle_guard.put(file_path, {}); self.analyzeDocument(file_doc) catch {}; - _ = cycle_guard.remove(file_path); } const file_sema = file_doc.sema orelse continue; if (imp.ns) |ns_name| { @@ -219,14 +218,9 @@ pub const DocumentStore = struct { continue; }; - // Cycle detection - if (cycle_guard.contains(imp.path)) continue; - // Ensure imported doc is analyzed if (imp_doc.sema == null) { - try cycle_guard.put(imp.path, {}); self.analyzeDocument(imp_doc) catch {}; - _ = cycle_guard.remove(imp.path); } const imp_sema = imp_doc.sema orelse continue; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 9da9c65..6644e0f 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -174,6 +174,7 @@ pub const Server = struct { // 1. Qualified name (e.g. "std.print" or UFCS "list.append") if (extractQualifiedName(doc.source, offset)) |qn| { + const qn_origin = sx.ast.Span{ .start = qn.full_start, .end = qn.full_end }; // Namespace import member if (self.findImportByNs(doc, qn.ns)) |imp| { if (self.documents.get(imp.path)) |imp_doc| { @@ -181,7 +182,7 @@ pub const Server = struct { if (imp_doc.sema) |imp_sema| { if (findSymbolByName(imp_sema.symbols, qn.member)) |si| { const sym = imp_sema.symbols[si]; - if (try self.sendSymbolLocation(id_json, imp_doc, sym)) return; + if (try self.sendSymbolLocationWithOrigin(id_json, imp_doc, sym, qn_origin)) return; } } } else { @@ -194,7 +195,7 @@ pub const Server = struct { if (dir_doc.sema) |dir_sema| { if (findSymbolByName(dir_sema.symbols, qn.member)) |si| { const sym = dir_sema.symbols[si]; - if (try self.sendSymbolLocation(id_json, dir_doc, sym)) return; + if (try self.sendSymbolLocationWithOrigin(id_json, dir_doc, sym, qn_origin)) return; } } } @@ -205,7 +206,7 @@ pub const Server = struct { if (findSymbolByName(sema.symbols, qn.member)) |si| { const sym = sema.symbols[si]; if (sym.kind == .function) { - if (try self.sendSymbolLocation(id_json, doc, sym)) return; + if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, qn_origin)) return; } } } @@ -215,14 +216,14 @@ pub const Server = struct { const ref = sema.references[ref_idx]; if (ref.symbol_index < sema.symbols.len) { const sym = sema.symbols[ref.symbol_index]; - if (try self.sendSymbolLocation(id_json, doc, sym)) return; + if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, ref.span)) return; } } // 3. Symbol definition name at offset if (findSymbolNameAtOffset(sema.symbols, doc.source, offset)) |sym_idx| { const sym = sema.symbols[sym_idx]; - if (try self.sendSymbolLocation(id_json, doc, sym)) return; + if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, sym.def_span)) return; } // 4. #import "path" string → open the file (or directory) @@ -256,7 +257,9 @@ pub const Server = struct { if (!is_qualified) { if (findSymbolByName(sema.symbols, name)) |si| { const sym = sema.symbols[si]; - if (try self.sendSymbolLocation(id_json, doc, sym)) return; + const name_end = name_start + @as(u32, @intCast(name.len)); + const origin = sx.ast.Span{ .start = name_start, .end = name_end }; + if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, origin)) return; } } } @@ -1112,6 +1115,11 @@ pub const Server = struct { return ST.type_; } + // Uppercase identifiers are conventionally types + if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') { + return ST.type_; + } + return null; } @@ -1270,20 +1278,34 @@ pub const Server = struct { /// Send a Location response for a symbol, resolving to the correct file via origin. fn sendSymbolLocation(self: *Server, id_json: []const u8, doc: *const Document, sym: sx.sema.Symbol) !bool { + return self.sendSymbolLocationWithOrigin(id_json, doc, sym, null); + } + + fn sendSymbolLocationWithOrigin(self: *Server, id_json: []const u8, doc: *const Document, sym: sx.sema.Symbol, origin_span: ?sx.ast.Span) !bool { if (sym.origin) |origin_path| { - // Symbol is from an imported file const origin_doc = self.documents.get(origin_path) orelse return false; - const range = spanToRange(origin_doc.source, sym.def_span); + const target_range = spanToRange(origin_doc.source, sym.def_span); const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{origin_path}); - const loc_json = try lsp.locationJson(self.allocator, target_uri, range); - try self.sendResponse(id_json, loc_json); + if (origin_span) |os| { + const src_range = spanToRange(doc.source, os); + const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); + try self.sendResponse(id_json, loc_json); + } else { + const loc_json = try lsp.locationJson(self.allocator, target_uri, target_range); + try self.sendResponse(id_json, loc_json); + } return true; } else { - // Symbol is local - const range = spanToRange(doc.source, sym.def_span); + const target_range = spanToRange(doc.source, sym.def_span); const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{doc.path}); - const loc_json = try lsp.locationJson(self.allocator, target_uri, range); - try self.sendResponse(id_json, loc_json); + if (origin_span) |os| { + const src_range = spanToRange(doc.source, os); + const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); + try self.sendResponse(id_json, loc_json); + } else { + const loc_json = try lsp.locationJson(self.allocator, target_uri, target_range); + try self.sendResponse(id_json, loc_json); + } return true; } } @@ -1775,7 +1797,7 @@ pub const Server = struct { return source[qstart + 1 .. qend]; } - pub fn extractQualifiedName(source: []const u8, offset: u32) ?struct { ns: []const u8, member: []const u8 } { + pub fn extractQualifiedName(source: []const u8, offset: u32) ?struct { ns: []const u8, member: []const u8, full_start: u32, full_end: u32 } { if (offset >= source.len) return null; var end: u32 = offset; @@ -1792,6 +1814,8 @@ pub const Server = struct { return .{ .ns = source[ns_start .. start - 1], .member = source[start..end], + .full_start = ns_start, + .full_end = end, }; } } @@ -1803,6 +1827,8 @@ pub const Server = struct { return .{ .ns = source[start..end], .member = source[end + 1 .. member_end], + .full_start = start, + .full_end = member_end, }; } } diff --git a/src/lsp/types.zig b/src/lsp/types.zig index 2f1cdbb..232e395 100644 --- a/src/lsp/types.zig +++ b/src/lsp/types.zig @@ -255,6 +255,23 @@ pub fn locationJson(allocator: std.mem.Allocator, uri: []const u8, range: Range) ); } +/// Build a LocationLink JSON response (for go-to-definition with origin range). +pub fn locationLinkJson(allocator: std.mem.Allocator, target_uri: []const u8, target_range: Range, origin_range: Range) ![]const u8 { + const uri_escaped = try jsonString(allocator, target_uri); + return std.fmt.allocPrint(allocator, + "[{{\"originSelectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}," ++ + "\"targetUri\":{s}," ++ + "\"targetRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}," ++ + "\"targetSelectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}]", + .{ + origin_range.start.line, origin_range.start.character, origin_range.end.line, origin_range.end.character, + uri_escaped, + target_range.start.line, target_range.start.character, target_range.end.line, target_range.end.character, + target_range.start.line, target_range.start.character, target_range.end.line, target_range.end.character, + }, + ); +} + /// Build a Hover JSON response. pub fn hoverJson(allocator: std.mem.Allocator, contents: []const u8) ![]const u8 { const escaped = try jsonString(allocator, contents); diff --git a/tests/expected/14-demo.txt b/tests/expected/14-demo.txt index 1f7bb9f..4dbb69d 100644 --- a/tests/expected/14-demo.txt +++ b/tests/expected/14-demo.txt @@ -1 +1 @@ -/Volumes/Store/dev/swipelab/sx/examples/14-demo.sx:16:1: error: comptime execution failed: UnsupportedExpression +[1.000000, 0.000000, -1.000000] diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index da58368..9919f35 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -362,4 +362,21 @@ hello from testpkg 14 hello world piped ok! +alloc len: 5 +alloc[0]: 10 +alloc[4]: 50 +bytes len: 3 +--- allocators --- +gpa allocs: 2 +gpa final: 0 +arena chunks: 1 +arena overflow: 2 +arena a1: 42 +arena a3: 99 +arena reset idx: 0 +arena reset gpa: 1 +arena deinit: 0 +buf pos: 48 +buf overflow: 0 +buf reset: 0 === DONE ===