...
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
154
examples/modules/allocators.sx
Normal file
154
examples/modules/allocators.sx
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
110
src/codegen.zig
110
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Volumes/Store/dev/swipelab/sx/examples/14-demo.sx:16:1: error: comptime execution failed: UnsupportedExpression
|
||||
[1.000000, 0.000000, -1.000000]
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user