diff --git a/examples/23-quicksort.sx b/examples/23-quicksort.sx index d986370..6346c79 100644 --- a/examples/23-quicksort.sx +++ b/examples/23-quicksort.sx @@ -8,16 +8,12 @@ quick_sort :: (items: []$T) { while j < hi { if items[j] < pivot { i += 1; - tmp := items[i]; - items[i] = items[j]; - items[j] = tmp; + items[i], items[j] = items[j], items[i]; } j += 1; } i += 1; - tmp := items[i]; - items[i] = items[hi]; - items[hi] = tmp; + items[i], items[hi] = items[hi], items[i]; i; } diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 4b7ff93..a60f2bc 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -1012,5 +1012,32 @@ END; print("flags-explicit: {}\n", wf); print("flags-explicit-raw: {}\n", cast(s64) wf); + // --- Multi-target assignment (swap) --- + print("--- swap ---\n"); + + // Variable swap + { + sa := 10; + sb := 20; + sa, sb = sb, sa; + print("var swap: {} {}\n", sa, sb); + } + + // Array element swap + { + sarr : [3]s64 = .[1, 2, 3]; + sarr[0], sarr[2] = sarr[2], sarr[0]; + print("arr swap: {} {}\n", sarr[0], sarr[2]); + } + + // 3-way rotation + { + ra := 1; + rb := 2; + rc := 3; + ra, rb, rc = rc, ra, rb; + print("3-way: {} {} {}\n", ra, rb, rc); + } + print("=== DONE ===\n"); } diff --git a/readme.md b/readme.md index c707006..1191178 100644 --- a/readme.md +++ b/readme.md @@ -23,16 +23,12 @@ quick_sort :: (items: []$T) { while j < hi { if items[j] < pivot { i += 1; - tmp := items[i]; - items[i] = items[j]; - items[j] = tmp; + items[i], items[j] = items[j], items[i]; } j += 1; } i += 1; - tmp := items[i]; - items[i] = items[hi]; - items[hi] = tmp; + items[i], items[hi] = items[hi], items[i]; i; } diff --git a/specs.md b/specs.md index 00f2522..c32e8f3 100644 --- a/specs.md +++ b/specs.md @@ -849,6 +849,7 @@ Statements are terminated by `;`. - **Declaration**: `name :: value;` / `name := value;` - **Assignment**: `name = value;` / `name += value;` (and other compound assignments). Also supports field targets: `obj.field = value;` +- **Multi-target assignment**: `a, b = b, a;` — all RHS values are evaluated before any stores, enabling swaps without temporaries. Target count must equal value count. Only plain `=` is supported (no compound operators). Each target must be a valid lvalue (variable, field, index, dereference). - **Expression statement**: `expr;` — evaluates the expression (last in a block = return value) - **Return**: `return expr;` — returns from the enclosing function with the given value. `return;` returns void. - **Break**: `break;` — exits a match arm or while loop @@ -1059,7 +1060,7 @@ field_group = IDENT (',' IDENT)* ':' type ('=' expr)? ';' params = param (',' param)* param = IDENT ':' type block = '{' stmt* '}' -stmt = decl | assignment ';' | return_stmt | defer_stmt | insert_stmt +stmt = decl | assignment ';' | multi_assign ';' | return_stmt | defer_stmt | insert_stmt | break_stmt | continue_stmt | expr ';' return_stmt = 'return' expr? ';' break_stmt = 'break' ';' @@ -1067,6 +1068,7 @@ continue_stmt = 'continue' ';' defer_stmt = 'defer' expr ';' insert_stmt = '#insert' expr ';' assignment = lvalue ('=' | '+=' | '-=' | '*=' | '/=') expr +multi_assign = lvalue (',' lvalue)+ '=' expr (',' expr)+ lvalue = IDENT | postfix '.' IDENT expr = if_expr | match_expr | while_expr | for_expr | lambda | binary while_expr = 'while' expr block diff --git a/src/ast.zig b/src/ast.zig index 3c0a874..bd42a71 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -30,6 +30,7 @@ pub const Node = struct { const_decl: ConstDecl, var_decl: VarDecl, assignment: Assignment, + multi_assign: MultiAssign, enum_decl: EnumDecl, struct_decl: StructDecl, struct_literal: StructLiteral, @@ -227,6 +228,11 @@ pub const Assignment = struct { }; }; +pub const MultiAssign = struct { + targets: []const *Node, + values: []const *Node, +}; + pub const EnumDecl = struct { name: []const u8, variant_names: []const []const u8, diff --git a/src/codegen.zig b/src/codegen.zig index 547ac82..861bf2a 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -2447,6 +2447,9 @@ pub const CodeGen = struct { .assignment => |asgn| { return self.genAssignment(asgn); }, + .multi_assign => |ma| { + return self.genMultiAssign(ma); + }, .return_stmt => |rs| { // Evaluate return value first, then emit all defers, then return if (rs.value) |val_node| { @@ -2945,6 +2948,65 @@ pub const CodeGen = struct { return null; } + fn genMultiAssign(self: *CodeGen, ma: ast.MultiAssign) !c.LLVMValueRef { + const n = ma.targets.len; + + // Phase 1: Evaluate ALL RHS values into temporaries. + // This ensures correctness for aliased swaps (a, b = b, a). + const tmp_ptrs = try self.allocator.alloc(c.LLVMValueRef, n); + const target_types = try self.allocator.alloc(Type, n); + + for (0..n) |i| { + target_types[i] = self.inferType(ma.targets[i]); + const llvm_ty = self.typeToLLVM(target_types[i]); + const val = try self.genExprAsType(ma.values[i], target_types[i]); + const tmp = self.buildEntryBlockAlloca(llvm_ty, "swap_tmp"); + _ = c.LLVMBuildStore(self.builder, val, tmp); + tmp_ptrs[i] = tmp; + } + + // Phase 2: Load temporaries and store to each target. + for (0..n) |i| { + const llvm_ty = self.typeToLLVM(target_types[i]); + const val = c.LLVMBuildLoad2(self.builder, llvm_ty, tmp_ptrs[i], "swap_load"); + try self.storeToLvalue(ma.targets[i], val); + } + + return null; + } + + fn storeToLvalue(self: *CodeGen, target: *Node, val: c.LLVMValueRef) !void { + // Deref assignment: p.* = val + if (target.data == .deref_expr) { + const de = target.data.deref_expr; + const ptr_val = try self.genExpr(de.operand); + _ = c.LLVMBuildStore(self.builder, val, ptr_val); + return; + } + + // Identifier assignment: x = val (with const check) + if (target.data == .identifier) { + const name = target.data.identifier.name; + const lookup = self.lookupValue(name) orelse + return self.emitErrorFmt("undefined variable '{s}'", .{name}); + const entry = lookup.asNamedValue() orelse + return self.emitErrorFmt("cannot assign to constant '{s}'", .{name}); + if (entry.is_const) + return self.emitErrorFmt("cannot assign to '{s}'", .{name}); + _ = c.LLVMBuildStore(self.builder, val, entry.ptr); + return; + } + + // Field access and index expressions — use genAddressOf to get the target pointer + if (target.data == .field_access or target.data == .index_expr) { + const ptr = try self.genAddressOf(target); + _ = c.LLVMBuildStore(self.builder, val, ptr); + return; + } + + return self.emitError("multi-assign target must be a variable, field, index, or dereference expression"); + } + fn genFieldAssignment(self: *CodeGen, asgn: ast.Assignment) !c.LLVMValueRef { const fa = asgn.target.data.field_access; @@ -3276,6 +3338,9 @@ pub const CodeGen = struct { .assignment => |asgn| { return self.genAssignment(asgn); }, + .multi_assign => |ma| { + return self.genMultiAssign(ma); + }, .return_stmt => |rs| { if (rs.value) |val_node| { const raw_val = try self.genExpr(val_node); diff --git a/src/parser.zig b/src/parser.zig index a8bd064..27f9510 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -834,6 +834,12 @@ pub const Parser = struct { return self.parseTypedBinding(name, start); } + // Multi-target assignment: ident, expr, ... = expr, expr, ...; + if (self.current.tag == .comma) { + const first_target = try self.createNode(start, .{ .identifier = .{ .name = name } }); + return try self.parseMultiAssign(first_target, start); + } + // Check for assignment operators if (self.isAssignOp()) { const op = self.assignOp(); @@ -918,6 +924,11 @@ pub const Parser = struct { // Expression statement const expr = try self.parseExpr(); + // Multi-target assignment: expr, expr, ... = expr, expr, ...; + if (self.current.tag == .comma) { + return try self.parseMultiAssign(expr, expr.span.start); + } + // Check for field assignment: expr = value; (e.g. a.b = 1;) if (self.isAssignOp()) { const op = self.assignOp(); @@ -1623,6 +1634,45 @@ pub const Parser = struct { }; } + fn parseMultiAssign(self: *Parser, first_target: *Node, start: u32) !*Node { + var targets = std.ArrayList(*Node).empty; + try targets.append(self.allocator, first_target); + + // Consume remaining targets separated by commas + while (self.current.tag == .comma) { + self.advance(); + const target = try self.parseExpr(); + try targets.append(self.allocator, target); + } + + // Only plain '=' is allowed + if (self.current.tag != .equal) { + return self.fail("multi-target assignment requires '='"); + } + self.advance(); + + // Parse RHS values separated by commas + var values = std.ArrayList(*Node).empty; + const first_val = try self.parseExpr(); + try values.append(self.allocator, first_val); + while (self.current.tag == .comma) { + self.advance(); + const val = try self.parseExpr(); + try values.append(self.allocator, val); + } + + if (targets.items.len != values.items.len) { + return self.fail("multi-target assignment: target count does not match value count"); + } + + try self.expect(.semicolon); + + return try self.createNode(start, .{ .multi_assign = .{ + .targets = try targets.toOwnedSlice(self.allocator), + .values = try values.toOwnedSlice(self.allocator), + } }); + } + fn binaryPrec(self: *const Parser) u8 { return switch (self.current.tag) { .kw_or => 1, diff --git a/src/sema.zig b/src/sema.zig index 51235b1..a3f334d 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -689,6 +689,10 @@ pub const Analyzer = struct { try self.analyzeNode(asgn.target); try self.analyzeNode(asgn.value); }, + .multi_assign => |ma| { + for (ma.targets) |t| try self.analyzeNode(t); + for (ma.values) |v| try self.analyzeNode(v); + }, .return_stmt => |ret| { if (ret.value) |val| { try self.analyzeNode(val); @@ -959,6 +963,14 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { if (findNodeAtOffset(asgn.target, offset)) |found| return found; if (findNodeAtOffset(asgn.value, offset)) |found| return found; }, + .multi_assign => |ma| { + for (ma.targets) |t| { + if (findNodeAtOffset(t, offset)) |found| return found; + } + for (ma.values) |v| { + if (findNodeAtOffset(v, offset)) |found| return found; + } + }, .return_stmt => |ret| { if (ret.value) |val| { if (findNodeAtOffset(val, offset)) |found| return found; diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index c3308cc..dd5ee13 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -236,4 +236,8 @@ flags-all: .read | .write | .execute flags-raw: 3 flags-explicit: .vsync | .resizable flags-explicit-raw: 68 +--- swap --- +var swap: 20 10 +arr swap: 3 1 +3-way: 3 1 2 === DONE ===