From 1ecac79642e3cd5c0a948c8e7cf4e50eaa9f96d1 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 20 Feb 2026 12:12:51 +0200 Subject: [PATCH] bit ops --- examples/50-smoke.sx | 35 +++++++++++++++++++++ specs.md | 42 ++++++++++++++++++++----- src/ast.zig | 9 ++++++ src/codegen.zig | 21 ++++++++++--- src/comptime.zig | 42 +++++++++++++++++++++++++ src/lexer.zig | 40 ++++++++++++++++++++++-- src/lsp/server.zig | 9 ++++++ src/parser.zig | 61 ++++++++++++++++++++++++++++--------- src/token.zig | 18 +++++++++++ tests/expected/50-smoke.txt | 17 +++++++++++ 10 files changed, 265 insertions(+), 29 deletions(-) diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 4c12132..730ed2d 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -192,6 +192,20 @@ END; print("band: {}\n", 0xFF & 0x0F); print("bor: {}\n", 1 | 2 | 4); + // Bitwise XOR + print("bxor: {}\n", 0xFF ^ 0x0F); + print("bxor2: {}\n", 6 ^ 3); + + // Bitwise NOT + print("bnot: {}\n", ~0); + print("bnot2: {}\n", ~1); + + // Shifts + print("shl: {}\n", 1 << 4); + print("shr: {}\n", 256 >> 4); + print("shl2: {}\n", 3 << 3); + print("shr2: {}\n", 255 >> 1); + // Bitwise on variables bv1 := 0xFF; bv2 := 0x0F; @@ -199,6 +213,27 @@ END; bv3 := 1; bv4 := 6; print("bor-var: {}\n", bv3 | bv4); + print("bxor-var: {}\n", bv1 ^ bv2); + print("shl-var: {}\n", bv3 << 4); + print("shr-var: {}\n", bv1 >> 4); + print("bnot-var: {}\n", ~bv2); + + // Bitwise compound assignment + bca := 0xFF; + bca &= 0x0F; + print("and-assign: {}\n", bca); + bco := 0x0F; + bco |= 0xF0; + print("or-assign: {}\n", bco); + bcx := 0xFF; + bcx ^= 0x0F; + print("xor-assign: {}\n", bcx); + bcs := 1; + bcs <<= 8; + print("shl-assign: {}\n", bcs); + bcr := 256; + bcr >>= 4; + print("shr-assign: {}\n", bcr); // Modulo on variables mv1 := 17; diff --git a/specs.md b/specs.md index 6e29252..8bda742 100644 --- a/specs.md +++ b/specs.md @@ -65,6 +65,10 @@ GLSL; | `>=` | greater or equal | | `&` | bitwise AND | | `\|` | bitwise OR | +| `^` | bitwise XOR | +| `~` | bitwise NOT (unary) | +| `<<` | left shift | +| `>>` | right shift (arithmetic for signed, logical for unsigned) | | `and` | logical AND (short-circuit) | | `or` | logical OR (short-circuit) | | `in` | membership test (tuples) | @@ -72,6 +76,11 @@ GLSL; | `-=` | sub-assign | | `*=` | mul-assign | | `/=` | div-assign | +| `&=` | bitwise AND assign | +| `\|=` | bitwise OR assign | +| `^=` | bitwise XOR assign | +| `<<=` | left shift assign | +| `>>=` | right shift assign | ### Delimiters and Punctuation @@ -768,11 +777,27 @@ Restrictions: ### Bitwise Operators -`&` (bitwise AND) and `|` (bitwise OR) work on all integer types, not just flags. They sit at precedence level 3, between comparisons and logical operators. +All bitwise operators work on integer types. `>>` is arithmetic (sign-extending) for signed types and logical (zero-filling) for unsigned types. ```sx -x := 0xFF & 0x0F; // 15 -y := 1 | 2 | 4; // 7 +x := 0xFF & 0x0F; // 15 — AND +y := 1 | 2 | 4; // 7 — OR +z := 0xFF ^ 0x0F; // 240 — XOR +w := ~0; // -1 — NOT +a := 1 << 4; // 16 — left shift +b := 256 >> 4; // 16 — right shift +``` + +Compound assignment forms: `&=`, `|=`, `^=`, `<<=`, `>>=`. + +```sx +x := 0xFF; +x &= 0x0F; // 15 +x |= 0xF0; // 255 +x ^= 0x0F; // 240 +y := 1; +y <<= 8; // 256 +y >>= 4; // 16 ``` --- @@ -785,10 +810,13 @@ Everything in `sx` is expression-oriented where possible. | Prec | Operators | Notes | |------|-----------|-------| -| 6 (highest) | `*`, `/`, `%` | multiplication, division, modulo | -| 5 | `+`, `-` | addition, subtraction | -| 4 | `<`, `<=`, `>`, `>=`, `==`, `!=` | comparisons (chainable) | -| 3 | `&`, `\|` | bitwise AND, bitwise OR | +| 9 (highest) | `*`, `/`, `%` | multiplication, division, modulo | +| 8 | `+`, `-` | addition, subtraction | +| 7 | `<<`, `>>` | shifts | +| 6 | `<`, `<=`, `>`, `>=`, `==`, `!=` | comparisons (chainable) | +| 5 | `&` | bitwise AND | +| 4 | `^` | bitwise XOR | +| 3 | `\|` | bitwise OR | | 2 | `and` | logical AND (short-circuit) | | 1 (lowest) | `or` | logical OR (short-circuit) | diff --git a/src/ast.zig b/src/ast.zig index de4ed3c..6ef6ed9 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -157,6 +157,9 @@ pub const BinaryOp = struct { or_op, bit_and, bit_or, + bit_xor, + shl, + shr, in_op, }; }; @@ -173,6 +176,7 @@ pub const UnaryOp = struct { pub const Op = enum { negate, not, + bit_not, xx, address_of, }; @@ -231,6 +235,11 @@ pub const Assignment = struct { mul_assign, div_assign, mod_assign, + and_assign, + or_assign, + xor_assign, + shl_assign, + shr_assign, }; }; diff --git a/src/codegen.zig b/src/codegen.zig index 95f8441..abe9784 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -3108,6 +3108,11 @@ pub const CodeGen = struct { .mul_assign => if (ty.isFloat()) c.LLVMBuildFMul(self.builder, cur, rhs, "multmp") else c.LLVMBuildMul(self.builder, cur, rhs, "multmp"), .div_assign => if (ty.isFloat()) c.LLVMBuildFDiv(self.builder, cur, rhs, "divtmp") else if (ty.isUnsigned()) c.LLVMBuildUDiv(self.builder, cur, rhs, "divtmp") else c.LLVMBuildSDiv(self.builder, cur, rhs, "divtmp"), .mod_assign => if (ty.isFloat()) c.LLVMBuildFRem(self.builder, cur, rhs, "modtmp") else if (ty.isUnsigned()) c.LLVMBuildURem(self.builder, cur, rhs, "modtmp") else c.LLVMBuildSRem(self.builder, cur, rhs, "modtmp"), + .and_assign => c.LLVMBuildAnd(self.builder, cur, rhs, "bandtmp"), + .or_assign => c.LLVMBuildOr(self.builder, cur, rhs, "bortmp"), + .xor_assign => c.LLVMBuildXor(self.builder, cur, rhs, "bxortmp"), + .shl_assign => c.LLVMBuildShl(self.builder, cur, rhs, "shltmp"), + .shr_assign => if (ty.isUnsigned()) c.LLVMBuildLShr(self.builder, cur, rhs, "shrtmp") else c.LLVMBuildAShr(self.builder, cur, rhs, "shrtmp"), .assign => unreachable, }; } @@ -3560,6 +3565,7 @@ pub const CodeGen = struct { c.LLVMBuildNeg(self.builder, operand, "negtmp"); }, .not => c.LLVMBuildNot(self.builder, operand, "nottmp"), + .bit_not => c.LLVMBuildNot(self.builder, operand, "bnottmp"), .xx, .address_of => unreachable, }; }, @@ -4460,15 +4466,17 @@ pub const CodeGen = struct { } // Bitwise op on enum type: recursively generate both sides with enum context - if (node.data == .binary_op and (node.data.binary_op.op == .bit_or or node.data.binary_op.op == .bit_and) and target_ty.isEnum()) { + if (node.data == .binary_op and (node.data.binary_op.op == .bit_or or node.data.binary_op.op == .bit_and or node.data.binary_op.op == .bit_xor) and target_ty.isEnum()) { const binop = node.data.binary_op; const lhs = try self.genExprAsType(binop.lhs, target_ty); const rhs = try self.genExprAsType(binop.rhs, target_ty); const b = self.builder; - return if (binop.op == .bit_or) - c.LLVMBuildOr(b, lhs, rhs, "bortmp") - else - c.LLVMBuildAnd(b, lhs, rhs, "bandtmp"); + return switch (binop.op) { + .bit_or => c.LLVMBuildOr(b, lhs, rhs, "bortmp"), + .bit_and => c.LLVMBuildAnd(b, lhs, rhs, "bandtmp"), + .bit_xor => c.LLVMBuildXor(b, lhs, rhs, "bxortmp"), + else => unreachable, + }; } // Enum/union literal assigned to union type: construct tagged enum @@ -5714,6 +5722,9 @@ 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"), + .bit_xor => c.LLVMBuildXor(b, lhs, rhs, "bxortmp"), + .shl => c.LLVMBuildShl(b, lhs, rhs, "shltmp"), + .shr => if (is_unsigned) c.LLVMBuildLShr(b, lhs, rhs, "shrtmp") else c.LLVMBuildAShr(b, lhs, rhs, "shrtmp"), .and_op, .or_op, .in_op => unreachable, }; } diff --git a/src/comptime.zig b/src/comptime.zig index 0bf5a15..1228899 100644 --- a/src/comptime.zig +++ b/src/comptime.zig @@ -217,6 +217,10 @@ pub const Instruction = union(enum) { // Bitwise bit_and, bit_or, + bit_xor, + bit_not, + shl, + shr, // Logic not, @@ -608,6 +612,9 @@ pub const Compiler = struct { .gte => .gte, .bit_and => .bit_and, .bit_or => .bit_or, + .bit_xor => .bit_xor, + .shl => .shl, + .shr => .shr, .and_op, .or_op, .in_op => unreachable, }); } @@ -667,6 +674,7 @@ pub const Compiler = struct { switch (unop.op) { .negate => try self.emit(.negate), .not => try self.emit(.not), + .bit_not => try self.emit(.bit_not), .xx => try self.emit(.unwrap_any), // autocast — unwraps any_val to inner value .address_of => unreachable, // handled above } @@ -735,6 +743,11 @@ pub const Compiler = struct { .mul_assign => .mul, .div_assign => .div, .mod_assign => .mod, + .and_assign => .bit_and, + .or_assign => .bit_or, + .xor_assign => .bit_xor, + .shl_assign => .shl, + .shr_assign => .shr, .assign => unreachable, }); } else { @@ -1248,6 +1261,35 @@ pub const VM = struct { try self.push(.{ .int_val = a.int_val | b.int_val }); } else return error.TypeError; }, + .bit_xor => { + const b = try self.pop(); + const a = try self.pop(); + if (a == .int_val and b == .int_val) { + try self.push(.{ .int_val = a.int_val ^ b.int_val }); + } else return error.TypeError; + }, + .shl => { + const b = try self.pop(); + const a = try self.pop(); + if (a == .int_val and b == .int_val) { + const shift: u6 = @intCast(@as(u64, @bitCast(b.int_val)) & 63); + try self.push(.{ .int_val = a.int_val << shift }); + } else return error.TypeError; + }, + .shr => { + const b = try self.pop(); + const a = try self.pop(); + if (a == .int_val and b == .int_val) { + const shift: u6 = @intCast(@as(u64, @bitCast(b.int_val)) & 63); + try self.push(.{ .int_val = a.int_val >> shift }); + } else return error.TypeError; + }, + .bit_not => { + const v = try self.pop(); + if (v == .int_val) { + try self.push(.{ .int_val = ~v.int_val }); + } else return error.TypeError; + }, .negate => { const v = try self.pop(); try self.push(switch (v) { diff --git a/src/lexer.zig b/src/lexer.zig index a89d40f..2570da3 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -173,9 +173,29 @@ pub const Lexer = struct { } return self.makeToken(.percent, start, self.index); }, - '&' => return self.makeToken(.ampersand, start, self.index), + '&' => { + if (self.peek() == '=') { + self.index += 1; + return self.makeToken(.ampersand_equal, start, self.index); + } + return self.makeToken(.ampersand, start, self.index); + }, '@' => return self.makeToken(.at, start, self.index), - '|' => return self.makeToken(.pipe, start, self.index), + '|' => { + if (self.peek() == '=') { + self.index += 1; + return self.makeToken(.pipe_equal, start, self.index); + } + return self.makeToken(.pipe, start, self.index); + }, + '^' => { + if (self.peek() == '=') { + self.index += 1; + return self.makeToken(.caret_equal, start, self.index); + } + return self.makeToken(.caret, start, self.index); + }, + '~' => return self.makeToken(.tilde, start, self.index), '!' => { if (self.peek() == '=') { self.index += 1; @@ -184,6 +204,14 @@ pub const Lexer = struct { return self.makeToken(.bang, start, self.index); }, '<' => { + if (self.peek() == '<') { + self.index += 1; + if (self.peek() == '=') { + self.index += 1; + return self.makeToken(.less_less_equal, start, self.index); + } + return self.makeToken(.less_less, start, self.index); + } if (self.peek() == '=') { self.index += 1; return self.makeToken(.less_equal, start, self.index); @@ -191,6 +219,14 @@ pub const Lexer = struct { return self.makeToken(.less, start, self.index); }, '>' => { + if (self.peek() == '>') { + self.index += 1; + if (self.peek() == '=') { + self.index += 1; + return self.makeToken(.greater_greater_equal, start, self.index); + } + return self.makeToken(.greater_greater, start, self.index); + } if (self.peek() == '=') { self.index += 1; return self.makeToken(.greater_equal, start, self.index); diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 4904cae..aa5d809 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1028,8 +1028,17 @@ pub const Server = struct { .percent, .percent_equal, .ampersand, + .ampersand_equal, .at, .pipe, + .pipe_equal, + .caret, + .caret_equal, + .tilde, + .less_less, + .less_less_equal, + .greater_greater, + .greater_greater_equal, .arrow, .fat_arrow, .colon_colon, diff --git a/src/parser.zig b/src/parser.zig index 3d535fa..39e4fb6 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1025,7 +1025,7 @@ pub const Parser = struct { // ---- Expression parsing (Pratt / precedence climbing) ---- pub fn parseExpr(self: *Parser) anyerror!*Node { - return self.parseBinary(0); + return self.parseBinary(Prec.none); } fn parseBinary(self: *Parser, min_prec: u8) anyerror!*Node { @@ -1088,6 +1088,12 @@ pub const Parser = struct { const operand = try self.parseUnary(); return try self.createNode(start, .{ .unary_op = .{ .op = .not, .operand = operand } }); } + if (self.current.tag == .tilde) { + const start = self.current.loc.start; + self.advance(); + const operand = try self.parseUnary(); + return try self.createNode(start, .{ .unary_op = .{ .op = .bit_not, .operand = operand } }); + } if (self.current.tag == .kw_xx) { const start = self.current.loc.start; self.advance(); @@ -1435,9 +1441,9 @@ pub const Parser = struct { const start = self.current.loc.start; self.advance(); // skip 'if' - // Parse condition at prec 5 (arithmetic+), leaving all comparisons + // Parse condition above comparison level, leaving comparisons // unconsumed for manual handling with match disambiguation. - var condition = try self.parseBinary(5); + var condition = try self.parseBinary(Prec.shift); // Handle comparisons with chain detection and match disambiguation. // All comparisons (< <= > >= == !=) are at the same precedence. @@ -1459,13 +1465,13 @@ pub const Parser = struct { // Chain followed by == { is an error — fall through to // regular comparison (will likely fail at parse time) } - const rhs = try self.parseBinary(5); + const rhs = try self.parseBinary(Prec.shift); try operands.append(self.allocator, rhs); try ops.append(self.allocator, .eq); } else { const cmp_op = self.binaryOp() orelse break; self.advance(); - const rhs = try self.parseBinary(5); + const rhs = try self.parseBinary(Prec.shift); try operands.append(self.allocator, rhs); try ops.append(self.allocator, cmp_op); } @@ -1488,7 +1494,7 @@ pub const Parser = struct { } // Handle and/or with proper Pratt precedence - condition = try self.parseBinaryRhs(condition, 1); + condition = try self.parseBinaryRhs(condition, Prec.logical_or); // Inline form: if cond then expr [else expr] if (self.current.tag == .kw_then) { @@ -1771,7 +1777,9 @@ pub const Parser = struct { fn isAssignOp(self: *const Parser) bool { return switch (self.current.tag) { - .equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal => true, + .equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal, + .ampersand_equal, .pipe_equal, .caret_equal, .less_less_equal, .greater_greater_equal, + => true, else => false, }; } @@ -1784,6 +1792,11 @@ pub const Parser = struct { .star_equal => .mul_assign, .slash_equal => .div_assign, .percent_equal => .mod_assign, + .ampersand_equal => .and_assign, + .pipe_equal => .or_assign, + .caret_equal => .xor_assign, + .less_less_equal => .shl_assign, + .greater_greater_equal => .shr_assign, else => unreachable, }; } @@ -1827,16 +1840,31 @@ pub const Parser = struct { } }); } + const Prec = struct { + const none: u8 = 0; + const logical_or: u8 = 1; // or + const logical_and: u8 = 2; // and + const bit_or: u8 = 3; // | + const bit_xor: u8 = 4; // ^ + const bit_and: u8 = 5; // & + const comparison: u8 = 6; // == != < <= > >= in + const shift: u8 = 7; // << >> + const additive: u8 = 8; // + - + const multiplicative: u8 = 9; // * / % + }; + fn binaryPrec(self: *const Parser) u8 { return switch (self.current.tag) { - .kw_or => 1, - .kw_and => 2, - .pipe => 3, - .ampersand => 3, - .equal_equal, .bang_equal, .less, .less_equal, .greater, .greater_equal, .kw_in => 4, - .plus, .minus => 5, - .star, .slash, .percent => 6, - else => 0, + .kw_or => Prec.logical_or, + .kw_and => Prec.logical_and, + .pipe => Prec.bit_or, + .caret => Prec.bit_xor, + .ampersand => Prec.bit_and, + .equal_equal, .bang_equal, .less, .less_equal, .greater, .greater_equal, .kw_in => Prec.comparison, + .less_less, .greater_greater => Prec.shift, + .plus, .minus => Prec.additive, + .star, .slash, .percent => Prec.multiplicative, + else => Prec.none, }; } @@ -1845,6 +1873,7 @@ pub const Parser = struct { .kw_and => .and_op, .kw_or => .or_op, .pipe => .bit_or, + .caret => .bit_xor, .ampersand => .bit_and, .plus => .add, .minus => .sub, @@ -1857,6 +1886,8 @@ pub const Parser = struct { .less_equal => .lte, .greater => .gt, .greater_equal => .gte, + .less_less => .shl, + .greater_greater => .shr, .kw_in => .in_op, else => null, }; diff --git a/src/token.zig b/src/token.zig index c6b6037..aa940d5 100644 --- a/src/token.zig +++ b/src/token.zig @@ -63,8 +63,17 @@ pub const Tag = enum { percent, // % percent_equal, // %= ampersand, // & + ampersand_equal, // &= at, // @ pipe, // | + pipe_equal, // |= + caret, // ^ + caret_equal, // ^= + tilde, // ~ + less_less, // << + less_less_equal, // <<= + greater_greater, // >> + greater_greater_equal, // >>= // Delimiters l_paren, // ( @@ -121,8 +130,17 @@ pub const Tag = enum { .percent => "%", .percent_equal => "%=", .ampersand => "&", + .ampersand_equal => "&=", .at => "@", .pipe => "|", + .pipe_equal => "|=", + .caret => "^", + .caret_equal => "^=", + .tilde => "~", + .less_less => "<<", + .less_less_equal => "<<=", + .greater_greater => ">>", + .greater_greater_equal => ">>=", .kw_null => "null", .l_paren => "(", .r_paren => ")", diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 2940a01..0773876 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -36,8 +36,25 @@ eq-chain: true eq-chain-f: false band: 15 bor: 7 +bxor: 240 +bxor2: 5 +bnot: -1 +bnot2: -2 +shl: 16 +shr: 16 +shl2: 24 +shr2: 127 band-var: 15 bor-var: 7 +bxor-var: 240 +shl-var: 16 +shr-var: 15 +bnot-var: -16 +and-assign: 15 +or-assign: 255 +xor-assign: 240 +shl-assign: 256 +shr-assign: 16 mod-var: 2 and: true and-false: false