diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 2547766..4c12132 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -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"); } diff --git a/specs.md b/specs.md index f27a5df..6e29252 100644 --- a/specs.md +++ b/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 diff --git a/src/ast.zig b/src/ast.zig index 0b12e05..de4ed3c 100644 --- a/src/ast.zig +++ b/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, +}; diff --git a/src/codegen.zig b/src/codegen.zig index 7485ee4..95f8441 100644 --- a/src/codegen.zig +++ b/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), }; diff --git a/src/comptime.zig b/src/comptime.zig index 3245159..0bf5a15 100644 --- a/src/comptime.zig +++ b/src/comptime.zig @@ -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; }, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index b0b0778..4904cae 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -993,6 +993,8 @@ pub const Server = struct { .kw_or, .kw_null, .kw_push, + .kw_ufcs, + .kw_in, .hash_run, .hash_import, .hash_insert, diff --git a/src/parser.zig b/src/parser.zig index 205db64..3d535fa 100644 --- a/src/parser.zig +++ b/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(); diff --git a/src/sema.zig b/src/sema.zig index 54f686b..96a4279 100644 --- a/src/sema.zig +++ b/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; }, diff --git a/src/token.zig b/src/token.zig index b60da10..c6b6037 100644 --- a/src/token.zig +++ b/src/token.zig @@ -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 { diff --git a/src/types.zig b/src/types.zig index 5600578..c612fa6 100644 --- a/src/types.zig +++ b/src/types.zig @@ -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) diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index fdfb277..2940a01 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -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 ===