tuples
This commit is contained in:
@@ -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
113
specs.md
@@ -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
|
||||
|
||||
24
src/ast.zig
24
src/ast.zig
@@ -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,
|
||||
};
|
||||
|
||||
473
src/codegen.zig
473
src/codegen.zig
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -993,6 +993,8 @@ pub const Server = struct {
|
||||
.kw_or,
|
||||
.kw_null,
|
||||
.kw_push,
|
||||
.kw_ufcs,
|
||||
.kw_in,
|
||||
.hash_run,
|
||||
.hash_import,
|
||||
.hash_insert,
|
||||
|
||||
119
src/parser.zig
119
src/parser.zig
@@ -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();
|
||||
|
||||
16
src/sema.zig
16
src/sema.zig
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user