This commit is contained in:
agra
2026-02-19 01:26:04 +02:00
parent fbf8a62362
commit e0e655cd36
11 changed files with 938 additions and 25 deletions

View File

@@ -1223,5 +1223,98 @@ END;
print("{}\n", ct_num_msg);
}
// --- Tuples ---
{
print("=== Tuples ===\n");
pair := (40, 2);
print("{}\n", pair.0);
print("{}\n", pair.1);
named := (x: 10, y: 20);
print("{}\n", named.x);
print("{}\n", named.0);
single := (42,);
print("{}\n", single.0);
zeroed : (s32, s32) = ---;
print("{}\n", zeroed.0);
print("{}\n", zeroed.1);
}
// --- UFCS Aliases ---
{
print("=== UFCS Aliases ===\n");
num_sum :: (a: s64, b: s64) -> s64 { a + b; }
sum :: ufcs num_sum;
print("{}\n", num_sum(40, 2)); // 42 — direct call
print("{}\n", sum(40, 2)); // 42 — alias direct call
print("{}\n", 40.sum(2)); // 42 — UFCS via alias
// Tuple UFCS splatting
print("{}\n", (40, 2).sum()); // 42 — full splat
print("{}\n", (40,).sum(2)); // 42 — partial splat
compute :: (a: s64, b: s64, c: s64, d: s64) -> s64 { a + b * c - d; }
calc :: ufcs compute;
print("{}\n", (1, 2, 3, 4).calc()); // 1+2*3-4 = 3
print("{}\n", (1, 2).calc(3, 4)); // same = 3
print("{}\n", 1.calc(2, 3, 4)); // same = 3
// Tuple return type
swap :: (a: s64, b: s64) -> (s64, s64) { (b, a); }
s := swap(1, 2);
a := s.0;
b := s.1;
print("{}\n", a); // 2
print("{}\n", b); // 1
wrap :: (x: s64) -> (s64) { (x,); }
t := wrap(99);
print("{}\n", t.0); // 99
}
// --- Tuple Operators ---
{
print("=== Tuple Operators ===\n");
// Equality
print("{}\n", (1, 2) == (1, 2)); // true
print("{}\n", (1, 2) == (1, 3)); // false
print("{}\n", (1, 2) != (1, 3)); // true
print("{}\n", (1, 2) != (1, 2)); // false
// Concatenation
c := (1, 2) + (3, 4);
print("{}\n", c.0); // 1
print("{}\n", c.1); // 2
print("{}\n", c.2); // 3
print("{}\n", c.3); // 4
// Repetition
r := (1, 2) * 3;
print("{}\n", r.0); // 1
print("{}\n", r.1); // 2
print("{}\n", r.2); // 1
print("{}\n", r.3); // 2
print("{}\n", r.4); // 1
print("{}\n", r.5); // 2
// Lexicographic comparison
print("{}\n", (1, 2) < (1, 3)); // true
print("{}\n", (1, 3) < (1, 2)); // false
print("{}\n", (1, 2) < (1, 2)); // false
print("{}\n", (1, 2) <= (1, 2)); // true
print("{}\n", (2, 0) > (1, 9)); // true
print("{}\n", (1, 2) >= (1, 2)); // true
// Membership
print("{}\n", 2 in (1, 2, 3)); // true
print("{}\n", 5 in (1, 2, 3)); // false
}
print("=== DONE ===\n");
}

113
specs.md
View File

@@ -45,7 +45,7 @@ GLSL;
```
### Keywords
`if`, `else`, `then`, `while`, `for`, `break`, `continue`, `true`, `false`, `enum`, `struct`, `union`, `case`, `return`, `defer`, `push`, `xx`, `and`, `or`
`if`, `else`, `then`, `while`, `for`, `break`, `continue`, `true`, `false`, `enum`, `struct`, `union`, `case`, `return`, `defer`, `push`, `ufcs`, `in`, `xx`, `and`, `or`
> Note: `enum` is used for both payload-less and payload-bearing sum types (tagged unions). `union` is reserved for C-style untagged unions (memory overlays).
@@ -67,6 +67,7 @@ GLSL;
| `\|` | bitwise OR |
| `and` | logical AND (short-circuit) |
| `or` | logical OR (short-circuit) |
| `in` | membership test (tuples) |
| `+=` | add-assign |
| `-=` | sub-assign |
| `*=` | mul-assign |
@@ -267,6 +268,83 @@ Struct values in string interpolation print as `TypeName{field:value, ...}`:
print("{}", v1); // Vec4{x:1.0, y:2.0, z:3.0, w:0.0}
```
### Tuple Types
Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned.
#### Construction
```sx
pair := (40, 2); // positional tuple: (s64, s64)
named := (x: 10, y: 20); // named tuple: (x: s64, y: s64)
single := (42,); // 1-tuple (trailing comma in value position)
zeroed : (s32, s32) = ---; // zero-initialized tuple
```
Note: In value position, `(expr)` without a comma is a grouping expression, not a tuple. Use `(expr,)` for a 1-tuple value.
#### Type Syntax
In type position, `(T)` is always a tuple type — no trailing comma needed. The `->` arrow disambiguates function types from tuple types:
```sx
(s64) // tuple type with one field
(s64, s64) // tuple type with two fields
(s64) -> s64 // function type: takes s64, returns s64
(s64, s64) -> s64 // function type: takes two s64, returns s64
```
#### Field Access
```sx
pair.0; // 40 — numeric index
pair.1; // 2
named.x; // 10 — named field
named.0; // 10 — numeric index also works on named tuples
```
#### As Return Type
```sx
swap :: (a: s64, b: s64) -> (s64, s64) { (b, a); }
wrap :: (x: s64) -> (s64) { (x,); }
s := swap(1, 2); // s.0 = 2, s.1 = 1
t := wrap(42); // t.0 = 42
```
#### Representation
Tuples are represented as anonymous LLVM struct types (same layout as named structs). A tuple `(s64, s64)` has LLVM type `{ i64, i64 }`.
#### Tuple Operators
**Equality and inequality** — element-wise comparison, both sides must have the same field count:
```sx
(1, 2) == (1, 2) // true
(1, 2) != (1, 3) // true
```
**Concatenation** (`+`) — creates a new tuple with fields from both sides:
```sx
c := (1, 2) + (3, 4); // c : (s64, s64, s64, s64)
c.0; // 1
c.3; // 4
```
**Repetition** (`*`) — repeats a tuple N times (N must be a compile-time integer literal):
```sx
r := (1, 2) * 3; // r : (s64, s64, s64, s64, s64, s64)
r.0; // 1
r.5; // 2
```
**Lexicographic comparison** (`<`, `<=`, `>`, `>=`) — compares element-by-element left to right:
```sx
(1, 2) < (1, 3) // true (first fields equal, 2 < 3)
(2, 0) > (1, 9) // true (2 > 1, rest ignored)
(1, 2) <= (1, 2) // true (all equal, <= allows tie)
```
**Membership** (`in`) — checks if a value exists in a tuple:
```sx
3 in (1, 2, 3) // true
5 in (1, 2, 3) // false
```
### Array Types
Fixed-size arrays with element type and length.
```sx
@@ -885,6 +963,39 @@ print("{}\n", p.point_sum()); // calls point_sum(p) → 7
UFCS works with pointer receivers (auto-deref applies) and generic functions. If the field name exists as both a struct field and a free function, the struct field takes priority.
#### UFCS Aliases
The `ufcs` keyword creates a name alias for a function, decoupling the method name from the function name:
```sx
arena_alloc :: (arena: *Arena, size: s64) -> *void { ... }
alloc :: ufcs arena_alloc;
myArena.alloc(42); // calls arena_alloc(myArena, 42)
alloc(myArena, 42); // also works as a direct call
```
This avoids the naming redundancy of `myArena.arena_alloc(42)`.
#### Tuple UFCS Splatting
When a tuple is used as the receiver of a UFCS call, its elements are unpacked as leading arguments:
```sx
num_add :: (a: s64, b: s64) -> s64 { a + b; }
add :: ufcs num_add;
(40, 2).add(); // splats to num_add(40, 2) → 42
(40,).add(2); // partial: num_add(40, 2) → 42
40.add(2); // normal UFCS: num_add(40, 2) → 42
```
With more arguments:
```sx
compute :: (a: s64, b: s64, c: s64, d: s64) -> s64 { a + b * c - d; }
calc :: ufcs compute;
(1, 2, 3, 4).calc(); // full splat → compute(1, 2, 3, 4)
(1, 2).calc(3, 4); // partial splat → compute(1, 2, 3, 4)
1.calc(2, 3, 4); // normal UFCS → compute(1, 2, 3, 4)
```
### Field Access
```sx
object.field

View File

@@ -65,6 +65,9 @@ pub const Node = struct {
foreign_expr: ForeignExpr,
library_decl: LibraryDecl,
function_type_expr: FunctionTypeExpr,
tuple_type_expr: TupleTypeExpr,
tuple_literal: TupleLiteral,
ufcs_alias: UfcsAlias,
pub fn declName(self: Data) ?[]const u8 {
return switch (self) {
@@ -75,6 +78,7 @@ pub const Node = struct {
.struct_decl => |d| d.name,
.union_decl => |d| d.name,
.namespace_decl => |d| d.name,
.ufcs_alias => |d| d.name,
else => null,
};
}
@@ -153,6 +157,7 @@ pub const BinaryOp = struct {
or_op,
bit_and,
bit_or,
in_op,
};
};
@@ -394,3 +399,22 @@ pub const FunctionTypeExpr = struct {
param_types: []const *Node,
return_type: ?*Node, // null = void return
};
pub const TupleTypeExpr = struct {
field_types: []const *Node,
field_names: ?[]const []const u8, // null for positional
};
pub const TupleLiteral = struct {
elements: []const TupleElement,
};
pub const TupleElement = struct {
name: ?[]const u8, // null for positional
value: *Node,
};
pub const UfcsAlias = struct {
name: []const u8,
target: []const u8,
};

View File

@@ -196,6 +196,10 @@ pub const CodeGen = struct {
global_mutable_vars: std.StringHashMap(NamedValue),
// Declared return types for non-generic functions (preserves signedness lost by LLVM round-trip)
function_return_types: std.StringHashMap(Type),
// Tuple alloca → type mapping (since tuples are anonymous, we track their types by alloca pointer)
tuple_alloca_types: std.AutoHashMap(usize, Type),
// UFCS alias map: alias name → target function name
ufcs_aliases: std.StringHashMap([]const u8),
// Target configuration (triple, cpu, opt level, lib paths, linker)
target_config: TargetConfig = .{},
// Cached primitive LLVM types (initialized once in init(), avoids repeated FFI calls)
@@ -405,6 +409,8 @@ pub const CodeGen = struct {
.foreign_name_map = std.StringHashMap([]const u8).init(allocator),
.global_mutable_vars = std.StringHashMap(NamedValue).init(allocator),
.function_return_types = std.StringHashMap(Type).init(allocator),
.tuple_alloca_types = std.AutoHashMap(usize, Type).init(allocator),
.ufcs_aliases = std.StringHashMap([]const u8).init(allocator),
.target_config = target_config,
.cached_i1 = c.LLVMInt1TypeInContext(ctx),
.cached_i8 = c.LLVMInt8TypeInContext(ctx),
@@ -576,6 +582,14 @@ pub const CodeGen = struct {
.pointer_type, .many_pointer_type, .function_type => self.ptrType(),
.any_type => self.getAnyStructType(),
.meta_type => self.ptrType(),
.tuple_type => |info| {
const n: c_uint = @intCast(info.field_types.len);
const field_llvm_types = self.allocator.alloc(c.LLVMTypeRef, info.field_types.len) catch unreachable;
for (info.field_types, 0..) |ft, i| {
field_llvm_types[i] = self.typeToLLVM(ft);
}
return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0);
},
};
}
@@ -1197,6 +1211,9 @@ pub const CodeGen = struct {
.var_decl => |vd| {
try self.registerGlobalVar(vd);
},
.ufcs_alias => |ua| {
try self.ufcs_aliases.put(ua.name, ua.target);
},
else => {},
}
}
@@ -1476,6 +1493,18 @@ pub const CodeGen = struct {
.return_type = ret_ptr,
} };
}
// Tuple type: (T1, T2) or (T1,)
if (tn.data == .tuple_type_expr) {
const tte = tn.data.tuple_type_expr;
const field_types = self.allocator.alloc(Type, tte.field_types.len) catch return .void_type;
for (tte.field_types, 0..) |ft, i| {
field_types[i] = self.resolveType(ft);
}
return .{ .tuple_type = .{
.field_types = field_types,
.field_names = tte.field_names,
} };
}
// Parameterized type: Vector(N, T) or generic struct instantiation
if (tn.data == .parameterized_type_expr) {
const pte = tn.data.parameterized_type_expr;
@@ -2357,6 +2386,9 @@ pub const CodeGen = struct {
const sname = self.resolveAlias(ret_type.struct_type);
const info = try self.getStructInfo(sname);
return c.LLVMBuildLoad2(self.builder, info.llvm_type, raw_val, "retval");
} else if (ret_type.isTuple()) {
const llvm_ty = self.typeToLLVM(ret_type);
return c.LLVMBuildLoad2(self.builder, llvm_ty, raw_val, "retval");
} else if (ret_type.isUnion()) {
const uname = ret_type.union_type;
const resolved = self.resolveAlias(uname);
@@ -2716,6 +2748,39 @@ pub const CodeGen = struct {
return null;
}
// Tuple-typed variable
if (sx_ty.isTuple()) {
const llvm_ty = self.typeToLLVM(sx_ty);
if (vd.value) |val| {
if (val.data == .undef_literal) {
// Zero-initialized tuple
const alloca = try self.buildNamedAlloca(llvm_ty, vd.name);
self.storeNull(llvm_ty, alloca);
try self.registerVariable(vd.name, alloca, sx_ty);
return null;
}
if (val.data == .tuple_literal) {
// Tuple literal — use its alloca directly
const lit_alloca = try self.genTupleLiteral(val.data.tuple_literal);
try self.registerVariable(vd.name, lit_alloca, sx_ty);
return null;
}
// General expression (e.g., function call returning a tuple, or tuple op)
const result = try self.genExpr(val);
// If the result is already a tuple alloca (from concat/repeat/etc), use it directly
if (self.tuple_alloca_types.contains(@intFromPtr(result))) {
try self.registerVariable(vd.name, result, sx_ty);
return null;
}
// Otherwise it's a loaded struct value (e.g., from function call) — store into an alloca
const alloca = try self.buildNamedAlloca(llvm_ty, vd.name);
_ = c.LLVMBuildStore(self.builder, result, alloca);
try self.registerVariable(vd.name, alloca, sx_ty);
return null;
}
return self.emitErrorFmt("tuple variable '{s}' must be initialized", .{vd.name});
}
// Union-typed variable (tagged enum or C-style union)
if (sx_ty.isUnion()) {
const uname = self.resolveAlias(sx_ty.union_type);
@@ -2977,6 +3042,22 @@ pub const CodeGen = struct {
return null;
}
// Tuple-typed constant: tuple literal returns alloca, use directly
if (sx_ty.isTuple()) {
if (cd.value.data == .tuple_literal) {
const lit_alloca = try self.genTupleLiteral(cd.value.data.tuple_literal);
try self.registerVariable(cd.name, lit_alloca, sx_ty);
return null;
}
// General expression (e.g., function call returning a tuple)
const val = try self.genExpr(cd.value);
const llvm_ty = self.typeToLLVM(sx_ty);
const alloca = try self.buildNamedAlloca(llvm_ty, cd.name);
_ = c.LLVMBuildStore(self.builder, val, alloca);
try self.registerVariable(cd.name, alloca, sx_ty);
return null;
}
// Function pointer typed constant
if (sx_ty.isFunctionType()) {
const llvm_ty = self.ptrType();
@@ -3422,6 +3503,31 @@ pub const CodeGen = struct {
return self.genStringComparison(binop.op, lhs, rhs);
}
// Tuple comparison: element-wise
if (lhs_ty.isTuple() and rhs_ty.isTuple() and
(binop.op == .eq or binop.op == .neq or binop.op == .lt or binop.op == .lte or binop.op == .gt or binop.op == .gte))
{
return self.genTupleComparison(binop.op, binop.lhs, binop.rhs, lhs_ty, rhs_ty);
}
// Tuple concatenation: tuple + tuple
if (lhs_ty.isTuple() and rhs_ty.isTuple() and binop.op == .add) {
return self.genTupleConcat(binop.lhs, binop.rhs, lhs_ty, rhs_ty);
}
// Tuple repetition: tuple * int
if (lhs_ty.isTuple() and rhs_ty.isInt() and binop.op == .mul) {
return self.genTupleRepeat(binop.lhs, binop.rhs, lhs_ty);
}
// Membership: value in tuple
if (binop.op == .in_op) {
if (rhs_ty.isTuple()) {
return self.genTupleMembership(binop.lhs, binop.rhs, rhs_ty);
}
return self.emitError("'in' requires a tuple on the right side");
}
const lhs = try self.genExprAsType(binop.lhs, result_type);
const rhs = try self.genExprAsType(binop.rhs, result_type);
return self.genBinaryOp(binop.op, lhs, rhs, result_type);
@@ -3470,6 +3576,9 @@ pub const CodeGen = struct {
const ctx_name: ?[]const u8 = if (self.current_return_type.isStruct()) self.current_return_type.struct_type else null;
return self.genStructLiteral(sl, ctx_name);
},
.tuple_literal => |tl| {
return self.genTupleLiteral(tl);
},
.array_literal => |al| {
// Typed array/vector/slice literal: Type.[elems]
if (al.type_expr) |te| {
@@ -3539,6 +3648,10 @@ pub const CodeGen = struct {
.const_decl => |cd| {
return self.genConstDecl(cd);
},
.ufcs_alias => |ua| {
try self.ufcs_aliases.put(ua.name, ua.target);
return null;
},
.assignment => |asgn| {
return self.genAssignment(asgn);
},
@@ -4181,6 +4294,60 @@ pub const CodeGen = struct {
return alloca;
}
/// Resolve a field name or numeric index to a tuple field index.
fn resolveTupleFieldIndex(info: Type.TupleTypeInfo, field: []const u8) ?usize {
// Try numeric index first: "0", "1", etc.
if (std.fmt.parseInt(usize, field, 10)) |idx| {
if (idx < info.field_types.len) return idx;
} else |_| {}
// Try named lookup
if (info.field_names) |names| {
for (names, 0..) |name, i| {
if (std.mem.eql(u8, name, field)) return i;
}
}
return null;
}
fn genTupleLiteral(self: *CodeGen, tl: ast.TupleLiteral) anyerror!c.LLVMValueRef {
const n = tl.elements.len;
// Infer types for each element
const field_types = try self.allocator.alloc(Type, n);
const field_llvm_types = try self.allocator.alloc(c.LLVMTypeRef, n);
for (tl.elements, 0..) |elem, i| {
field_types[i] = self.inferType(elem.value);
field_llvm_types[i] = self.typeToLLVM(field_types[i]);
}
// Build anonymous LLVM struct type
const llvm_type = c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, @intCast(n), 0);
// Alloca and store each element
const alloca = self.buildEntryBlockAlloca(llvm_type, "tuple");
for (tl.elements, 0..) |elem, i| {
const val = try self.genExprAsType(elem.value, field_types[i]);
self.storeStructField(llvm_type, alloca, @intCast(i), val);
}
// Store the tuple type info for later field access
const field_names = if (tl.elements[0].name != null) blk: {
const names = try self.allocator.alloc([]const u8, n);
for (tl.elements, 0..) |elem, i| {
names[i] = elem.name orelse "";
}
break :blk @as(?[]const []const u8, names);
} else null;
// Register this alloca as having tuple type
const tuple_ty = Type{ .tuple_type = .{
.field_names = field_names,
.field_types = field_types,
} };
try self.tuple_alloca_types.put(@intFromPtr(alloca), tuple_ty);
return alloca;
}
/// Generate an array literal as an alloca with elements stored via GEP.
/// If target_ty is provided, elements are converted to the array's element type.
/// Otherwise, element type is inferred from the first element.
@@ -5270,6 +5437,14 @@ pub const CodeGen = struct {
// GEP to payload area, load as variant type
return self.loadStructField(info.llvm_type, entry.ptr, info.payload_field_index, self.typeToLLVM(variant_ty));
}
if (entry.ty.isTuple()) {
const ti = entry.ty.tuple_type;
const idx = resolveTupleFieldIndex(ti, fa.field) orelse
return self.emitErrorFmt("no field '{s}' in tuple", .{fa.field});
const llvm_ty = self.typeToLLVM(entry.ty);
const field_llvm_ty = self.typeToLLVM(ti.field_types[idx]);
return self.loadStructField(llvm_ty, entry.ptr, @intCast(idx), field_llvm_ty);
}
if (entry.ty.isVector()) {
const vec_val = self.loadTyped(entry.ty, entry.ptr, "vec_load");
return self.genVectorExtract(vec_val, fa.field);
@@ -5334,6 +5509,21 @@ pub const CodeGen = struct {
}
}
}
if (obj_ty.isTuple()) {
const ti = obj_ty.tuple_type;
const idx = resolveTupleFieldIndex(ti, fa.field) orelse
return self.emitErrorFmt("no field '{s}' in tuple", .{fa.field});
const llvm_ty = self.typeToLLVM(obj_ty);
const field_llvm_ty = self.typeToLLVM(ti.field_types[idx]);
// If obj is a tuple literal, genExpr returned an alloca pointer directly
if (fa.object.data == .tuple_literal) {
return self.loadStructField(llvm_ty, obj_val, @intCast(idx), field_llvm_ty);
}
// Otherwise obj_val is a loaded struct value — store into tmp and GEP
const tmp = self.buildEntryBlockAlloca(llvm_ty, "tmp_tuple");
_ = c.LLVMBuildStore(self.builder, obj_val, tmp);
return self.loadStructField(llvm_ty, tmp, @intCast(idx), field_llvm_ty);
}
return self.emitError("field access on non-struct/non-vector expression");
}
@@ -5524,7 +5714,7 @@ pub const CodeGen = struct {
.gte => if (is_float) c.LLVMBuildFCmp(b, c.LLVMRealOGE, lhs, rhs, "getmp") else if (is_unsigned) c.LLVMBuildICmp(b, c.LLVMIntUGE, lhs, rhs, "getmp") else c.LLVMBuildICmp(b, c.LLVMIntSGE, lhs, rhs, "getmp"),
.bit_and => c.LLVMBuildAnd(b, lhs, rhs, "bandtmp"),
.bit_or => c.LLVMBuildOr(b, lhs, rhs, "bortmp"),
.and_op, .or_op => unreachable,
.and_op, .or_op, .in_op => unreachable,
};
}
@@ -5577,6 +5767,198 @@ pub const CodeGen = struct {
return phi;
}
fn genTupleComparison(self: *CodeGen, op: ast.BinaryOp.Op, lhs_node: *ast.Node, rhs_node: *ast.Node, lhs_ty: Type, rhs_ty: Type) !c.LLVMValueRef {
const lhs_info = lhs_ty.tuple_type;
const rhs_info = rhs_ty.tuple_type;
const n = lhs_info.field_types.len;
if (n != rhs_info.field_types.len) return self.emitError("tuple comparison requires same field count");
const lhs_val = try self.genExpr(lhs_node);
const rhs_val = try self.genExpr(rhs_node);
const lhs_llvm_ty = self.typeToLLVM(lhs_ty);
const rhs_llvm_ty = self.typeToLLVM(rhs_ty);
const i1_ty = self.i1Type();
// Equality: AND-reduce field-wise == comparisons
if (op == .eq or op == .neq) {
var result = c.LLVMConstInt(i1_ty, 1, 0); // start with true
for (0..n) |i| {
const field_ty = lhs_info.field_types[i];
const field_llvm_ty = self.typeToLLVM(field_ty);
const lf = self.loadStructField(lhs_llvm_ty, lhs_val, @intCast(i), field_llvm_ty);
const rf = self.loadStructField(rhs_llvm_ty, rhs_val, @intCast(i), field_llvm_ty);
const cmp = self.genBinaryOp(.eq, lf, rf, field_ty);
result = c.LLVMBuildAnd(self.builder, result, cmp, "tuple_eq_and");
}
if (op == .neq) return c.LLVMBuildNot(self.builder, result, "tuple_neq");
return result;
}
// Lexicographic comparison: chain of basic blocks
// For < : at each field, if lhs.i < rhs.i → true, if lhs.i > rhs.i → false, else continue
// For > : swap the sense
// For <= : same as < but tie → true
// For >= : same as > but tie → true
const is_less = (op == .lt or op == .lte);
const tie_result: u64 = if (op == .lte or op == .gte) 1 else 0;
const merge_bb = self.appendBB("tup.cmp.merge");
const phi_count = 2 * n + 1;
const phi_vals = try self.allocator.alloc(c.LLVMValueRef, phi_count);
const phi_bbs = try self.allocator.alloc(c.LLVMBasicBlockRef, phi_count);
var phi_idx: usize = 0;
for (0..n) |i| {
const field_ty = lhs_info.field_types[i];
const field_llvm_ty = self.typeToLLVM(field_ty);
const lf = self.loadStructField(lhs_llvm_ty, lhs_val, @intCast(i), field_llvm_ty);
const rf = self.loadStructField(rhs_llvm_ty, rhs_val, @intCast(i), field_llvm_ty);
// Check if lhs.i < rhs.i (or > if !is_less)
const less_op: ast.BinaryOp.Op = if (is_less) .lt else .gt;
const cmp_less = self.genBinaryOp(less_op, lf, rf, field_ty);
phi_vals[phi_idx] = c.LLVMConstInt(i1_ty, 1, 0); // true
phi_bbs[phi_idx] = self.getCurrentBlock();
phi_idx += 1;
const next_bb = self.appendBB("tup.cmp.next");
_ = c.LLVMBuildCondBr(self.builder, cmp_less, merge_bb, next_bb);
c.LLVMPositionBuilderAtEnd(self.builder, next_bb);
// Check if lhs.i > rhs.i (or < if !is_less)
const greater_op: ast.BinaryOp.Op = if (is_less) .gt else .lt;
const cmp_greater = self.genBinaryOp(greater_op, lf, rf, field_ty);
phi_vals[phi_idx] = c.LLVMConstInt(i1_ty, 0, 0); // false
phi_bbs[phi_idx] = self.getCurrentBlock();
phi_idx += 1;
const eq_bb = self.appendBB("tup.cmp.eq");
_ = c.LLVMBuildCondBr(self.builder, cmp_greater, merge_bb, eq_bb);
c.LLVMPositionBuilderAtEnd(self.builder, eq_bb);
}
// All fields equal — tie
phi_vals[phi_idx] = c.LLVMConstInt(i1_ty, tie_result, 0);
phi_bbs[phi_idx] = self.getCurrentBlock();
phi_idx += 1;
_ = c.LLVMBuildBr(self.builder, merge_bb);
c.LLVMPositionBuilderAtEnd(self.builder, merge_bb);
const phi = c.LLVMBuildPhi(self.builder, i1_ty, "tup_cmp");
c.LLVMAddIncoming(phi, phi_vals.ptr, @ptrCast(phi_bbs.ptr), @intCast(phi_idx));
return phi;
}
fn genTupleConcat(self: *CodeGen, lhs_node: *ast.Node, rhs_node: *ast.Node, lhs_ty: Type, rhs_ty: Type) !c.LLVMValueRef {
const lhs_info = lhs_ty.tuple_type;
const rhs_info = rhs_ty.tuple_type;
const n_lhs = lhs_info.field_types.len;
const n_rhs = rhs_info.field_types.len;
const n_total = n_lhs + n_rhs;
// Build new tuple type
const field_types = try self.allocator.alloc(Type, n_total);
const field_llvm_types = try self.allocator.alloc(c.LLVMTypeRef, n_total);
for (0..n_lhs) |i| {
field_types[i] = lhs_info.field_types[i];
field_llvm_types[i] = self.typeToLLVM(field_types[i]);
}
for (0..n_rhs) |i| {
field_types[n_lhs + i] = rhs_info.field_types[i];
field_llvm_types[n_lhs + i] = self.typeToLLVM(field_types[n_lhs + i]);
}
const result_llvm_ty = c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, @intCast(n_total), 0);
const alloca = self.buildEntryBlockAlloca(result_llvm_ty, "tuple_cat");
// Copy fields from lhs
const lhs_val = try self.genExpr(lhs_node);
const lhs_llvm_ty = self.typeToLLVM(lhs_ty);
for (0..n_lhs) |i| {
const fv = self.loadStructField(lhs_llvm_ty, lhs_val, @intCast(i), field_llvm_types[i]);
self.storeStructField(result_llvm_ty, alloca, @intCast(i), fv);
}
// Copy fields from rhs
const rhs_val = try self.genExpr(rhs_node);
const rhs_llvm_ty = self.typeToLLVM(rhs_ty);
for (0..n_rhs) |i| {
const fv = self.loadStructField(rhs_llvm_ty, rhs_val, @intCast(i), field_llvm_types[n_lhs + i]);
self.storeStructField(result_llvm_ty, alloca, @intCast(n_lhs + i), fv);
}
// Register tuple type
const result_ty = Type{ .tuple_type = .{ .field_types = field_types, .field_names = null } };
try self.tuple_alloca_types.put(@intFromPtr(alloca), result_ty);
return alloca;
}
fn genTupleRepeat(self: *CodeGen, tuple_node: *ast.Node, count_node: *ast.Node, tuple_ty: Type) !c.LLVMValueRef {
// Count must be a comptime int literal
const count: usize = switch (count_node.data) {
.int_literal => |il| @intCast(il.value),
else => return self.emitError("tuple repetition count must be a compile-time integer literal"),
};
if (count == 0) return self.emitError("tuple repetition count must be positive");
const info = tuple_ty.tuple_type;
const n_fields = info.field_types.len;
const n_total = n_fields * count;
// Build new tuple type
const field_types = try self.allocator.alloc(Type, n_total);
const field_llvm_types = try self.allocator.alloc(c.LLVMTypeRef, n_total);
for (0..count) |r| {
for (0..n_fields) |f| {
field_types[r * n_fields + f] = info.field_types[f];
field_llvm_types[r * n_fields + f] = self.typeToLLVM(info.field_types[f]);
}
}
const result_llvm_ty = c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, @intCast(n_total), 0);
const alloca = self.buildEntryBlockAlloca(result_llvm_ty, "tuple_rep");
// Generate tuple value once
const tuple_val = try self.genExpr(tuple_node);
const tuple_llvm_ty = self.typeToLLVM(tuple_ty);
// Copy fields for each repetition
for (0..count) |r| {
for (0..n_fields) |f| {
const fv = self.loadStructField(tuple_llvm_ty, tuple_val, @intCast(f), field_llvm_types[r * n_fields + f]);
self.storeStructField(result_llvm_ty, alloca, @intCast(r * n_fields + f), fv);
}
}
const result_ty = Type{ .tuple_type = .{ .field_types = field_types, .field_names = null } };
try self.tuple_alloca_types.put(@intFromPtr(alloca), result_ty);
return alloca;
}
fn genTupleMembership(self: *CodeGen, value_node: *ast.Node, tuple_node: *ast.Node, tuple_ty: Type) !c.LLVMValueRef {
const info = tuple_ty.tuple_type;
const n = info.field_types.len;
if (n == 0) return c.LLVMConstInt(self.i1Type(), 0, 0);
const value = try self.genExpr(value_node);
const tuple_val = try self.genExpr(tuple_node);
const tuple_llvm_ty = self.typeToLLVM(tuple_ty);
const value_ty = self.inferType(value_node);
const i1_ty = self.i1Type();
// OR-reduce: (field0 == val) OR (field1 == val) OR ...
var result = c.LLVMConstInt(i1_ty, 0, 0); // start with false
for (0..n) |i| {
const field_ty = info.field_types[i];
const field_llvm_ty = self.typeToLLVM(field_ty);
const fv = self.loadStructField(tuple_llvm_ty, tuple_val, @intCast(i), field_llvm_ty);
const common_ty = Type.widen(value_ty, field_ty);
const cmp = self.genBinaryOp(.eq, value, fv, common_ty);
result = c.LLVMBuildOr(self.builder, result, cmp, "tuple_in_or");
}
return result;
}
fn genShortCircuitOp(self: *CodeGen, binop: ast.BinaryOp, is_and: bool) !c.LLVMValueRef {
const lhs_val = self.valueToBool(try self.genExpr(binop.lhs));
const lhs_bb = self.getCurrentBlock();
@@ -5681,16 +6063,46 @@ pub const CodeGen = struct {
// UFCS: obj.method(args...) → method(obj, args...)
const method_name = fa.field;
const method_z = self.allocator.dupeZ(u8, method_name) catch method_name;
if (self.generic_templates.contains(method_name) or
const resolved_method = self.ufcs_aliases.get(method_name) orelse method_name;
const method_z = self.allocator.dupeZ(u8, resolved_method) catch resolved_method;
if (self.generic_templates.contains(resolved_method) or
c.LLVMGetNamedFunction(self.module, method_z.ptr) != null)
{
// Check if receiver is a tuple — if so, splat its elements as leading args
const receiver_type = self.inferType(fa.object);
const is_tuple = receiver_type == .tuple_type;
if (is_tuple) {
const tuple_info = receiver_type.tuple_type;
const n_tuple = tuple_info.field_types.len;
var ufcs_args = try self.allocator.alloc(*Node, n_tuple + call_node.args.len);
// Create synthetic field_access nodes for each tuple element
for (0..n_tuple) |i| {
const syn_node = try self.allocator.create(ast.Node);
var idx_buf: [20]u8 = undefined;
const idx_str = std.fmt.bufPrint(&idx_buf, "{d}", .{i}) catch "0";
const field_name = try self.allocator.dupe(u8, idx_str);
syn_node.* = .{
.span = fa.object.span,
.data = .{ .field_access = .{ .object = fa.object, .field = field_name } },
};
ufcs_args[i] = syn_node;
}
for (call_node.args, 0..) |arg, i| {
ufcs_args[n_tuple + i] = arg;
}
return self.genCallByName(resolved_method, .{
.callee = call_node.callee,
.args = ufcs_args,
});
}
var ufcs_args = try self.allocator.alloc(*Node, call_node.args.len + 1);
ufcs_args[0] = fa.object;
for (call_node.args, 0..) |arg, i| {
ufcs_args[i + 1] = arg;
}
return self.genCallByName(method_name, .{
return self.genCallByName(resolved_method, .{
.callee = call_node.callee,
.args = ufcs_args,
});
@@ -5704,6 +6116,11 @@ pub const CodeGen = struct {
}
fn genCallByName(self: *CodeGen, callee_name: []const u8, call_node: ast.Call) !c.LLVMValueRef {
// Resolve UFCS alias: add :: ufcs num_add; → redirect "add" to "num_add"
if (self.ufcs_aliases.get(callee_name)) |resolved| {
return self.genCallByName(resolved, call_node);
}
// Check if this is a generic function call
if (self.generic_templates.get(callee_name)) |template| {
return self.genGenericCall(callee_name, template, call_node);
@@ -7424,10 +7841,36 @@ pub const CodeGen = struct {
.comptime_expr => |ct| self.inferType(ct.expr),
.binary_op => |binop| {
switch (binop.op) {
.eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op => return .boolean,
.eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op, .in_op => return .boolean,
else => {
const lhs_ty = self.inferType(binop.lhs);
const rhs_ty = self.inferType(binop.rhs);
// Tuple concatenation: (A, B) + (C, D) → (A, B, C, D)
if (lhs_ty.isTuple() and rhs_ty.isTuple() and binop.op == .add) {
const li = lhs_ty.tuple_type;
const ri = rhs_ty.tuple_type;
const n = li.field_types.len + ri.field_types.len;
const ft = self.allocator.alloc(Type, n) catch return .void_type;
for (0..li.field_types.len) |i| ft[i] = li.field_types[i];
for (0..ri.field_types.len) |i| ft[li.field_types.len + i] = ri.field_types[i];
return .{ .tuple_type = .{ .field_types = ft, .field_names = null } };
}
// Tuple repetition: (A, B) * 3 → (A, B, A, B, A, B)
if (lhs_ty.isTuple() and rhs_ty.isInt() and binop.op == .mul) {
const li = lhs_ty.tuple_type;
const count: usize = switch (binop.rhs.data) {
.int_literal => |il| @intCast(il.value),
else => return .void_type,
};
const n = li.field_types.len * count;
const ft = self.allocator.alloc(Type, n) catch return .void_type;
for (0..count) |r| {
for (0..li.field_types.len) |f| {
ft[r * li.field_types.len + f] = li.field_types[f];
}
}
return .{ .tuple_type = .{ .field_types = ft, .field_names = null } };
}
return Type.widen(lhs_ty, rhs_ty);
},
}
@@ -7628,6 +8071,12 @@ pub const CodeGen = struct {
if (obj_ty.isVector()) {
return obj_ty.vectorElementType() orelse Type.s(64);
}
if (obj_ty.isTuple()) {
const ti = obj_ty.tuple_type;
if (resolveTupleFieldIndex(ti, fa.field)) |idx| {
return ti.field_types[idx];
}
}
if (obj_ty.isStruct()) {
if (self.lookupStructInfo(obj_ty.struct_type)) |info| {
if (self.findNameIndex(info.field_names, fa.field)) |idx| {
@@ -7688,6 +8137,20 @@ pub const CodeGen = struct {
if (sl.struct_name) |sname| return .{ .struct_type = sname };
return Type.s(64);
},
.tuple_literal => |tl| {
const field_types = self.allocator.alloc(Type, tl.elements.len) catch return Type.s(64);
for (tl.elements, 0..) |elem, i| {
field_types[i] = self.inferType(elem.value);
}
const field_names: ?[]const []const u8 = if (tl.elements.len > 0 and tl.elements[0].name != null) blk: {
const names = self.allocator.alloc([]const u8, tl.elements.len) catch return Type.s(64);
for (tl.elements, 0..) |elem, i| {
names[i] = elem.name orelse "";
}
break :blk names;
} else null;
return .{ .tuple_type = .{ .field_names = field_names, .field_types = field_types } };
},
.while_expr, .for_expr, .break_expr, .continue_expr => .void_type,
else => Type.s(64),
};

View File

@@ -608,7 +608,7 @@ pub const Compiler = struct {
.gte => .gte,
.bit_and => .bit_and,
.bit_or => .bit_or,
.and_op, .or_op => unreachable,
.and_op, .or_op, .in_op => unreachable,
});
}
},
@@ -997,7 +997,7 @@ pub const Compiler = struct {
.null_literal => {
try self.emit(.push_null);
},
.pointer_type_expr, .many_pointer_type_expr => {
.pointer_type_expr, .many_pointer_type_expr, .tuple_type_expr => {
try self.emit(.push_void); // type expressions not meaningful as values
},
.undef_literal => {
@@ -1022,6 +1022,21 @@ pub const Compiler = struct {
.local_names = try names.toOwnedSlice(self.allocator),
} });
},
.tuple_literal => |tl| {
for (tl.elements) |elem| {
try self.compileNode(elem.value);
}
const fnames = try self.allocator.alloc([]const u8, tl.elements.len);
for (tl.elements, 0..) |elem, i| {
fnames[i] = elem.name orelse "";
}
try self.emit(.{ .make_struct = .{
.type_name = "__tuple",
.field_count = @intCast(tl.elements.len),
.field_names = fnames,
} });
},
.ufcs_alias => {}, // UFCS aliases are resolved at codegen, no-op in comptime
else => {
return error.UnsupportedExpression;
},

View File

@@ -993,6 +993,8 @@ pub const Server = struct {
.kw_or,
.kw_null,
.kw_push,
.kw_ufcs,
.kw_in,
.hash_run,
.hash_import,
.hash_insert,

View File

@@ -165,6 +165,18 @@ pub const Parser = struct {
return self.parseUnionDecl(name, start_pos);
}
// UFCS alias: name :: ufcs target;
if (self.current.tag == .kw_ufcs) {
self.advance();
if (self.current.tag != .identifier) {
return self.fail("expected function name after 'ufcs'");
}
const target = self.tokenSlice(self.current);
self.advance();
try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .ufcs_alias = .{ .name = name, .target = target } });
}
// Function declaration: (params) -> type { body } or () { body }
if (self.current.tag == .l_paren) {
// Look ahead: is this a function or an expression starting with `(`?
@@ -307,25 +319,32 @@ pub const Parser = struct {
self.advance();
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = true } });
}
// Function pointer type: (ParamTypes) -> ReturnType
// Function type: (ParamTypes) -> ReturnType
// Tuple type: (T1, T2) or (T1) — no '->' after ')'
if (self.current.tag == .l_paren) {
self.advance(); // skip '('
var param_types = std.ArrayList(*Node).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
}
try param_types.append(self.allocator, try self.parseTypeExpr());
}
try self.expect(.r_paren);
var return_type: ?*Node = null;
if (self.current.tag == .arrow) {
// '->' present: function type
self.advance(); // skip '->'
return_type = try self.parseTypeExpr();
const return_type = try self.parseTypeExpr();
return try self.createNode(start, .{ .function_type_expr = .{
.param_types = try param_types.toOwnedSlice(self.allocator),
.return_type = return_type,
} });
}
return try self.createNode(start, .{ .function_type_expr = .{
.param_types = try param_types.toOwnedSlice(self.allocator),
.return_type = return_type,
// No '->': tuple type (even for single element)
return try self.createNode(start, .{ .tuple_type_expr = .{
.field_types = try param_types.toOwnedSlice(self.allocator),
.field_names = null,
} });
}
@@ -1162,14 +1181,18 @@ pub const Parser = struct {
// Dereference: expr.*
self.advance();
expr = try self.createNode(expr.span.start, .{ .deref_expr = .{ .operand = expr } });
} else {
// Field access
if (self.current.tag != .identifier) {
return self.fail("expected field name after '.'");
}
} else if (self.current.tag == .identifier) {
// Named field access: expr.field
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
} else if (self.current.tag == .int_literal) {
// Numeric field access: tuple.0, tuple.1
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
} else {
return self.fail("expected field name or index after '.'");
}
} else if (self.current.tag == .l_bracket) {
// Index or slice access: expr[expr] or expr[start..end]
@@ -1314,11 +1337,30 @@ pub const Parser = struct {
if (self.isLambda()) {
return self.parseLambda();
}
// Grouped expression
self.advance();
const expr = try self.parseExpr();
self.advance(); // skip '('
// Check for named tuple: (name: expr, ...)
if (self.current.tag == .identifier and self.peekNext() == .colon) {
return self.parseTupleLiteralNamed(start);
}
// Empty parens or first expression
if (self.current.tag == .r_paren) {
self.advance();
// () — empty tuple
return try self.createNode(start, .{ .tuple_literal = .{ .elements = &.{} } });
}
const first = try self.parseExpr();
// Check for comma → tuple
if (self.current.tag == .comma) {
return self.finishTupleAfterFirst(start, first);
}
// No comma → grouping
try self.expect(.r_paren);
return expr;
return first;
},
.kw_f32, .kw_f64, .kw_Type => {
// Type keyword used as expression (for type aliases: SOME_TYPE :: f64;)
@@ -1607,6 +1649,42 @@ pub const Parser = struct {
return try self.createNode(start_pos, .{ .match_expr = .{ .subject = subject, .arms = try arms.toOwnedSlice(self.allocator) } });
}
/// Parse a named tuple literal: (name: expr, name: expr, ...)
/// Called after '(' has been consumed and we've verified identifier + colon pattern.
fn parseTupleLiteralNamed(self: *Parser, start: u32) anyerror!*Node {
var elements = std.ArrayList(ast.TupleElement).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (self.current.tag != .identifier) {
return self.fail("expected field name in named tuple");
}
const name = self.tokenSlice(self.current);
self.advance();
try self.expect(.colon);
const value = try self.parseExpr();
try elements.append(self.allocator, .{ .name = name, .value = value });
if (self.current.tag == .comma) {
self.advance();
} else break;
}
try self.expect(.r_paren);
return try self.createNode(start, .{ .tuple_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
}
/// Finish parsing a tuple after the first positional element and a comma.
/// Called with first element already parsed and current token is ','.
fn finishTupleAfterFirst(self: *Parser, start: u32, first: *Node) anyerror!*Node {
var elements = std.ArrayList(ast.TupleElement).empty;
try elements.append(self.allocator, .{ .name = null, .value = first });
while (self.current.tag == .comma) {
self.advance(); // skip ','
if (self.current.tag == .r_paren) break; // trailing comma: (42,)
const value = try self.parseExpr();
try elements.append(self.allocator, .{ .name = null, .value = value });
}
try self.expect(.r_paren);
return try self.createNode(start, .{ .tuple_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
}
/// Save state, skip past matching parens, return the tag of the next token, then restore.
/// Returns null if no matching ')' found before EOF.
fn peekPastParens(self: *Parser) ?Tag {
@@ -1755,7 +1833,7 @@ pub const Parser = struct {
.kw_and => 2,
.pipe => 3,
.ampersand => 3,
.equal_equal, .bang_equal, .less, .less_equal, .greater, .greater_equal => 4,
.equal_equal, .bang_equal, .less, .less_equal, .greater, .greater_equal, .kw_in => 4,
.plus, .minus => 5,
.star, .slash, .percent => 6,
else => 0,
@@ -1779,6 +1857,7 @@ pub const Parser = struct {
.less_equal => .lte,
.greater => .gt,
.greater_equal => .gte,
.kw_in => .in_op,
else => null,
};
}
@@ -1797,6 +1876,14 @@ pub const Parser = struct {
};
}
/// Peek at the next token's tag without consuming.
fn peekNext(self: *Parser) Tag {
const saved_lexer = self.lexer;
const tok = self.lexer.next();
self.lexer = saved_lexer;
return tok.tag;
}
fn advance(self: *Parser) void {
self.prev_end = self.current.loc.end;
self.current = self.lexer.next();

View File

@@ -337,7 +337,7 @@ pub const Analyzer = struct {
.comptime_expr => |ct| self.inferExprType(ct.expr),
.binary_op => |binop| {
switch (binop.op) {
.eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op => return .boolean,
.eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op, .in_op => return .boolean,
else => {
const lhs_ty = self.inferExprType(binop.lhs);
const rhs_ty = self.inferExprType(binop.rhs);
@@ -783,7 +783,14 @@ pub const Analyzer = struct {
.parameterized_type_expr,
.index_expr,
.slice_expr,
.tuple_type_expr,
.ufcs_alias,
=> {},
.tuple_literal => |tl| {
for (tl.elements) |elem| {
try self.analyzeNode(elem.value);
}
},
.deref_expr => |de| {
try self.analyzeNode(de.operand);
},
@@ -1062,7 +1069,14 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
.parameterized_type_expr,
.index_expr,
.slice_expr,
.tuple_type_expr,
.ufcs_alias,
=> {},
.tuple_literal => |tl| {
for (tl.elements) |elem| {
if (findNodeAtOffset(elem.value, offset)) |found| return found;
}
},
.deref_expr => |de| {
if (findNodeAtOffset(de.operand, offset)) |found| return found;
},

View File

@@ -30,6 +30,8 @@ pub const Tag = enum {
kw_Type, // Type (metatype keyword)
kw_null, // null
kw_push, // push
kw_ufcs, // ufcs
kw_in, // in
// Symbols
colon, // :
@@ -181,6 +183,8 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{
.{ "Type", .kw_Type },
.{ "null", .kw_null },
.{ "push", .kw_push },
.{ "ufcs", .kw_ufcs },
.{ "in", .kw_in },
});
pub fn getKeyword(bytes: []const u8) ?Tag {

View File

@@ -24,6 +24,7 @@ pub const Type = union(enum) {
function_type: FunctionTypeInfo,
any_type,
meta_type: MetaTypeInfo,
tuple_type: TupleTypeInfo,
pub const SliceTypeInfo = struct {
element_name: []const u8,
@@ -56,6 +57,11 @@ pub const Type = union(enum) {
name: []const u8,
};
pub const TupleTypeInfo = struct {
field_names: ?[]const []const u8, // null for positional tuples
field_types: []const Type,
};
/// Content-based equality: compares string fields by content, not pointer identity.
pub fn eql(self: Type, other: Type) bool {
const Tag = std.meta.Tag(Type);
@@ -85,6 +91,20 @@ pub const Type = union(enum) {
return info.return_type.eql(o.return_type.*);
},
.meta_type => |info| std.mem.eql(u8, info.name, other.meta_type.name),
.tuple_type => |info| {
const o = other.tuple_type;
if (info.field_types.len != o.field_types.len) return false;
for (info.field_types, o.field_types) |a, b| {
if (!a.eql(b)) return false;
}
// If both have names, compare them
if (info.field_names != null and o.field_names != null) {
for (info.field_names.?, o.field_names.?) |a, b| {
if (!std.mem.eql(u8, a, b)) return false;
}
}
return true;
},
};
}
@@ -185,6 +205,13 @@ pub const Type = union(enum) {
};
}
pub fn isTuple(self: Type) bool {
return switch (self) {
.tuple_type => true,
else => false,
};
}
pub fn isAny(self: Type) bool {
return switch (self) {
.any_type => true,
@@ -344,6 +371,17 @@ pub const Type = union(enum) {
return std.mem.eql(u8, target.pointer_type.pointee_name, "void");
}
// Tuple → tuple: same field count and each field implicitly convertible
if (self.isTuple() and target.isTuple()) {
const si = self.tuple_type;
const ti = target.tuple_type;
if (si.field_types.len != ti.field_types.len) return false;
for (si.field_types, ti.field_types) |sf, tf| {
if (!sf.isImplicitlyConvertibleTo(tf)) return false;
}
return true;
}
const src_float = self.isFloat();
const dst_float = target.isFloat();
const src_int = self.isInt();
@@ -424,6 +462,20 @@ pub const Type = union(enum) {
return try buf.toOwnedSlice(allocator);
},
.meta_type => |info| info.name,
.tuple_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '(');
for (info.field_types, 0..) |ft, i| {
if (i > 0) try buf.appendSlice(allocator, ", ");
if (info.field_names) |names| {
try buf.appendSlice(allocator, names[i]);
try buf.appendSlice(allocator, ": ");
}
try buf.appendSlice(allocator, try ft.displayName(allocator));
}
try buf.append(allocator, ')');
return try buf.toOwnedSlice(allocator);
},
};
}
@@ -433,6 +485,11 @@ pub const Type = union(enum) {
// Same type → return it
if (a.eql(b)) return a;
// Tuple + tuple → return a if same field count
if (a.isTuple() and b.isTuple()) {
if (a.tuple_type.field_types.len == b.tuple_type.field_types.len) return a;
}
// Vector + vector of same dimensions → return a
if (a.isVector() and b.isVector()) return a;
// Vector + scalar → return vector (scalar will be broadcast)

View File

@@ -292,4 +292,47 @@ sprite-r: 255
sprite-scale: 1
say: hello (len=5)
n=42
=== Tuples ===
40
2
10
10
42
0
0
=== UFCS Aliases ===
42
42
42
42
42
3
3
3
2
1
99
=== Tuple Operators ===
true
false
true
false
1
2
3
4
1
2
1
2
1
2
true
false
false
true
true
true
true
false
=== DONE ===