feat: support compound updates and bitwise operators

This commit is contained in:
2026-10-30 00:18:44 +01:00
parent 670ba8f756
commit d7e637dc3b
20 changed files with 890 additions and 88 deletions
+95 -33
View File
@@ -30,7 +30,7 @@ State :: struct {
next_token usize
paren_depth usize = 0
allow_record_literals bool = true
allow_sum_types bool = true
allow_bit_or bool = true
match_arm_body bool = false
}
@@ -254,16 +254,16 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
open :: try consume(state, .open_curly)
previous_depth :: state.paren_depth
previous_record_literals :: state.allow_record_literals
previous_sum_types :: state.allow_sum_types
previous_bit_or :: state.allow_bit_or
previous_match_arm_body :: state.match_arm_body
state.paren_depth = 0
state.allow_record_literals = true
state.allow_sum_types = true
state.allow_bit_or = true
state.match_arm_body = false
defer {
state.paren_depth = previous_depth
state.allow_record_literals = previous_record_literals
state.allow_sum_types = previous_sum_types
state.allow_bit_or = previous_bit_or
state.match_arm_body = previous_match_arm_body
}
statements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
@@ -325,10 +325,10 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.break, .continue: return try parse_loop_control(state, allow_else)
.defer, .errdefer: return try parse_cleanup(state, allow_else)
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct, .c_struct, .opaque, .c_func,
.ident, .int, .float, .string, .true, .false, .minus, .bang, .tilde, .open_paren, .open_bracket, .struct, .c_struct, .opaque, .c_func,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .undefined, .try, .dollar: {
expression :: try parse_expr(state)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
if (is_assignment_operator(current(state).kind)) return try parse_assignment(state, expression, allow_else)
return try ast.add_expression_stmt(state.builder, expression)
}
else: return .unexpected_token
@@ -431,8 +431,9 @@ parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseErr
@hide:file
parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast.NodeId ! ParseError {
_ = try consume(state, .equal)
value :: try parse_expr(state)
operator :: assignment_operator(current(state).kind)
state.next_token += 1
value := try parse_expr(state)
if (
!next_is(state, .newline)
and !next_is(state, .close_curly)
@@ -440,17 +441,15 @@ parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast.
and !(allow_else and next_is(state, .else))
and !(state.match_arm_body and next_is(state, .comma))
) return .unexpected_token
end :: state.tokens[state.next_token - 1].span.end
return try ast.add_assignment_stmt(
state.builder,
ast.Span{
start = ast.builder_span(state.builder, target).start,
end = end,
source = state.builder.source_id,
},
target,
value,
)
span :: ast.Span{
start = ast.builder_span(state.builder, target).start,
end = state.tokens[state.next_token - 1].span.end,
source = state.builder.source_id,
}
if (operator) |binary| value = try ast.add_binary(state.builder, span, binary, target, value)
id :: try ast.add_assignment_stmt(state.builder, span, target, value)
if (operator != null) ast.set_assignment_compound(state.builder, id)
return id
}
@hide:file
@@ -657,13 +656,13 @@ parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
@hide:file
parse_guard_expr proc(state @mut State) ast.NodeId ! ParseError {
previous :: state.allow_record_literals
previous_sum_types :: state.allow_sum_types
previous_bit_or :: state.allow_bit_or
state.allow_record_literals = false
# capture pipes stay boundaries even when an index/call permits record literals.
state.allow_sum_types = false
# top-level header pipes delimit captures; nested expressions may use bitwise OR.
state.allow_bit_or = false
defer {
state.allow_record_literals = previous
state.allow_sum_types = previous_sum_types
state.allow_bit_or = previous_bit_or
}
return try parse_expr(state)
}
@@ -689,7 +688,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError {
and !next_is(state, .for) and !next_is(state, .inline)
and !next_is(state, .break) and !next_is(state, .continue)
) {
if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break
if (next_is(state, .ident) and is_assignment_operator(state.tokens[state.next_token + 1].kind)) break
state.next_token += 1
}
}
@@ -952,7 +951,7 @@ starts_type proc(kind lexer.TokenKind) bool {
member_name proc(kind lexer.TokenKind) bool {
return match kind {
.ident, .if, .else, .for, .inline, .while, .proc, .return, .yield, .break, .continue,
.true, .false, .and, .or, .mut, .struct, .c_struct, .opaque, .enum, .union, .distinct,
.true, .false, .and, .or, .xor, .mut, .struct, .c_struct, .opaque, .enum, .union, .distinct,
.alias, .match, .import, .test, .c_func, .null, .undefined, .try, .catch, .defer, .errdefer: true
else: false
}
@@ -1193,7 +1192,7 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError {
body = try parse_arm(state, false)
} else {
value :: try parse_expr(state)
if next_is(state, .equal) {
if (is_assignment_operator(current(state).kind)) {
statement :: try parse_assignment(state, value, false)
statements [1]ast.NodeId :: [statement]
body = try ast.add_block(state.builder, ast.builder_span(state.builder, statement), statements[..])
@@ -1391,6 +1390,7 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
}
operator ast.UnaryOperator :: match token.kind {
.bang: .logical_not
.tilde: .bit_not
.ampersand: .address_of
else: .negate
}
@@ -1477,11 +1477,10 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .bang or (token.kind == .pipe and state.allow_sum_types)) {
power :: if (token.kind == .bang) BindingPowerScalar(1) else BindingPowerScalar(2)
if (power < min_bp) break
if (token.kind == .bang) {
if (BindingPowerScalar(1) < min_bp) break
state.next_token += 1
right :: try parse_expr_bp(state, BindingPowerScalar(u32(power) + 1))
right :: try parse_type(state)
left = try ast.add_binary_type(state.builder, ast.Span{
start = state.tokens[expression_start].span.start,
end = state.tokens[state.next_token - 1].span.end,
@@ -1509,6 +1508,7 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .pipe and !state.allow_bit_or) break
if (infix_binding_power(token.kind)) |power| {
if (power.left < min_bp) break
state.next_token += 1
@@ -1553,6 +1553,9 @@ validate_range_bound proc(
parse_call proc(state @mut State, callee ast.NodeId, start u32, builtin bool) ast.NodeId ! ParseError {
_ = try consume(state, .open_paren)
previous :: state.allow_record_literals
previous_bit_or :: state.allow_bit_or
state.allow_bit_or = true
defer state.allow_bit_or = previous_bit_or
state.allow_record_literals = true
defer state.allow_record_literals = previous
state.paren_depth += 1
@@ -1591,6 +1594,9 @@ parse_call proc(state @mut State, callee ast.NodeId, start u32, builtin bool) as
parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! ParseError {
_ = try consume(state, .open_bracket)
previous :: state.allow_record_literals
previous_bit_or :: state.allow_bit_or
state.allow_bit_or = previous_bit_or or index_has_close(state)
defer state.allow_bit_or = previous_bit_or
state.allow_record_literals = true
defer state.allow_record_literals = previous
first ?ast.NodeId := null
@@ -1621,6 +1627,22 @@ parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! Pars
}, base, first?)
}
@hide:file
index_has_close proc(state @State) bool {
# keep malformed guard indices from consuming a capture list and its body.
depth := usize(1)
for (state.next_token)..(state.tokens.len) |index| {
kind :: state.tokens[index].kind
if (kind == .eof or (kind == .newline and depth == 1)) return false
if (kind == .open_paren or kind == .open_bracket or kind == .open_curly) depth += 1
if (kind == .close_paren or kind == .close_bracket or kind == .close_curly) {
depth -= 1
if (depth == 0) return kind == .close_bracket
}
}
return false
}
@hide:file
array_starts_type proc(state @State) bool {
depth usize := 0
@@ -1650,6 +1672,9 @@ array_starts_type proc(state @State) bool {
parse_array_literal proc(state @mut State) ast.NodeId ! ParseError {
open :: try consume(state, .open_bracket)
previous :: state.allow_record_literals
previous_bit_or :: state.allow_bit_or
state.allow_bit_or = true
defer state.allow_bit_or = previous_bit_or
state.allow_record_literals = true
defer state.allow_record_literals = previous
elements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
@@ -1708,7 +1733,7 @@ pointer_binding_starts proc(state @State) bool {
}
if (depth != 0) continue
if (kind == .double_colon or kind == .colon_equal) return true
if (kind == .newline or kind == .eof or kind == .equal) return false
if (kind == .newline or kind == .eof or is_assignment_operator(kind)) return false
}
return false
}
@@ -1744,6 +1769,7 @@ brace_starts_block proc(state @State) bool {
and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) return true
if (depth == 1) {
if (kind == .double_colon or kind == .colon_equal) return true
if (kind != .equal and is_assignment_operator(kind)) return true
previous :: if (index > first) state.tokens[index - 1].kind else lexer.TokenKind.open_curly
if (previous != .dot and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) {
match kind {
@@ -1777,6 +1803,9 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
state.next_token += 1
_ = try consume(state, .open_paren)
previous :: state.allow_record_literals
previous_bit_or :: state.allow_bit_or
state.allow_bit_or = true
defer state.allow_bit_or = previous_bit_or
state.allow_record_literals = true
defer state.allow_record_literals = previous
state.paren_depth += 1
@@ -1812,6 +1841,9 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
.open_paren: {
state.next_token += 1
previous :: state.allow_record_literals
previous_bit_or :: state.allow_bit_or
state.allow_bit_or = true
defer state.allow_bit_or = previous_bit_or
state.allow_record_literals = true
defer state.allow_record_literals = previous
state.paren_depth += 1
@@ -1910,7 +1942,7 @@ skip_operand_newlines proc(state @mut State) void {
# a missing operand must not consume the declaration or assignment on the next line.
if (
named_binding_starts(state)
or (next_is(state, .ident) and next_n_is(state, 1, .equal))
or (next_is(state, .ident) and is_assignment_operator(state.tokens[state.next_token + 1].kind))
) state.next_token = start
}
@@ -2083,7 +2115,7 @@ consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! Syn
@hide:file
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
return match kind {
.minus, .bang, .ampersand, .try: BindingPowerScalar(50)
.minus, .bang, .tilde, .ampersand, .try: BindingPowerScalar(50)
else: null
}
}
@@ -2095,6 +2127,8 @@ infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
.and: BindingPower{ left = BindingPowerScalar(4), right = BindingPowerScalar(5) }
.equal_equal, .not_equal: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
.less, .less_equal, .greater, .greater_equal: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
.ampersand, .xor, .pipe: BindingPower{ left = BindingPowerScalar(22), right = BindingPowerScalar(23) }
.shift_left, .shift_right, .shift_left_saturating: BindingPower{ left = BindingPowerScalar(25), right = BindingPowerScalar(26) }
.plus, .minus: BindingPower{ left = BindingPowerScalar(30), right = BindingPowerScalar(31) }
.star, .slash: BindingPower{ left = BindingPowerScalar(40), right = BindingPowerScalar(41) }
else: null
@@ -2110,6 +2144,12 @@ binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
.minus: .subtract
.star: .multiply
.slash: .divide
.ampersand: .bit_and
.pipe: .bit_or
.xor: .bit_xor
.shift_left: .shift_left
.shift_right: .shift_right
.shift_left_saturating: .shift_left_saturating
.equal_equal: .equal
.not_equal: .not_equal
.less: .less
@@ -2119,3 +2159,25 @@ binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
else: unreachable
}
}
@hide:file
is_assignment_operator proc(kind lexer.TokenKind) bool {
return kind == .equal or assignment_operator(kind) != null
}
@hide:file
assignment_operator proc(kind lexer.TokenKind) ?ast.BinaryOperator {
return match kind {
.plus_equal: .add
.minus_equal: .subtract
.star_equal: .multiply
.slash_equal: .divide
.ampersand_equal: .bit_and
.pipe_equal: .bit_or
.xor_equal: .bit_xor
.shift_left_equal: .shift_left
.shift_right_equal: .shift_right
.shift_left_saturating_equal: .shift_left_saturating
else: null
}
}
+134 -1
View File
@@ -1544,7 +1544,7 @@ preserves_array_error_ids_control_bodies_and_following_siblings test {
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` bad [~]i64 :: [~, 2]
` bad [%]i64 :: [%, 2]
` while (items[0) { break }
` for items[0 |i| { continue }
` unfinished :: [1,
@@ -2242,3 +2242,136 @@ keeps_yielding_match_and_catch_bodies_as_raw_blocks test {
try testing.expect_equal(ast.ExprKind.comptime_expr, ast.expr_kind(&tree, constant))
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, ast.comptime_operand(&tree, constant)))
}
parses_bitwise_precedence_and_left_associativity test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source :: "bits :: ~a + b * c << d >> e <<| f & g xor h | i < j == k and l or m\n"
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
expression := ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer
operators [11]ast.BinaryOperator :: [
.logical_or, .logical_and, .equal, .less,
.bit_or, .bit_xor, .bit_and,
.shift_left_saturating, .shift_right, .shift_left, .add,
]
for (operators) |operator| {
binary :: ast.binary_expr(&tree, expression)
try testing.expect_equal(operator, binary.operator)
if (operator == .add) {
product :: ast.binary_expr(&tree, binary.right)
try testing.expect_equal(ast.BinaryOperator.multiply, product.operator)
}
expression = binary.left
}
complement :: ast.unary_expr(&tree, expression)
try testing.expect_equal(ast.UnaryOperator.bit_not, complement.operator)
try testing.expect_equal("a", ast.source_text(&tree, ast.node_span(&tree, complement.operand)))
}
parses_compound_operators_in_statement_contexts test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`run proc() void {
` items[next()].field += rhs()
` value -= 1
` value *= 2
` value /= 3
` value &= mask
` value |= mask
` value xor= mask
` value <<= count
` value >>= count
` value <<|= count
` value = 4
` match value { 0: value += 1, else: value -= 1 }
` result :: { value |= mask }
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
builder := ast.init_builder(mem.c_allocator, source)
defer ast.deinit_builder(&builder)
root :: try parse_source(&builder, scan_state.tokens.items, &diagnostic_store)
tree := ast.view(&builder, root)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
operators [10]ast.BinaryOperator :: [
.add, .subtract, .multiply, .divide, .bit_and, .bit_or, .bit_xor,
.shift_left, .shift_right, .shift_left_saturating,
]
for (operators) |operator, index| {
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, index))
try testing.expect(assignment.compound)
binary :: ast.binary_expr(&tree, assignment.value)
try testing.expect_equal(operator, binary.operator)
}
try testing.expect(!ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 10)).compound)
match_value :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 11))
for (usize(0)..usize(2)) |index| {
arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, match_value, index))
try testing.expect(!arm.expression)
try testing.expect(ast.assignment_stmt(&tree, ast.block_statement(&tree, arm.body, 0)).compound)
}
block_value :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 12)).initializer
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, block_value))
}
keeps_type_composition_and_capture_pipes_with_bitwise_or test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`Alias :: A | B
`Error :: A | B
`Fallible :: T ! (A | B)
`run proc(value A | B) T ! (A | B) {
` if (flags | mask) |item| { item }
` while (flags | mask) |item| { break }
` for (flags | mask) |item, index| { continue }
` if value |item| { item }
` while value |item| { break }
` for values |item| { continue }
` if check_flags(flags | mask) { return }
` while values[flags | mask] { break }
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
for (usize(0)..usize(2)) |index| {
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, index)).initializer
try testing.expect_equal(ast.BinaryOperator.bit_or, ast.binary_expr(&tree, expression).operator)
}
fallible :: ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer
try testing.expect_equal(ast.ExprKind.fallible_type, ast.expr_kind(&tree, fallible))
try testing.expect_equal(ast.ExprKind.sum_type, ast.expr_kind(&tree, ast.binary_expr(&tree, fallible).right))
function_id :: ast.package_decl(&tree, 3)
annotation :: ast.param(&tree, ast.function_param(&tree, function_id, 0)).type
try testing.expect_equal(ast.ExprKind.sum_type, ast.expr_kind(&tree, annotation))
body :: ast.function_decl(&tree, function_id).body
condition :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect(condition.capture != null)
try testing.expect_equal(ast.BinaryOperator.bit_or, ast.binary_expr(&tree, condition.condition).operator)
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1))
try testing.expect(loop.capture != null)
try testing.expect_equal(ast.BinaryOperator.bit_or, ast.binary_expr(&tree, loop.condition).operator)
iteration :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 2))
try testing.expect(iteration.capture != null and iteration.index != null)
try testing.expect_equal(ast.BinaryOperator.bit_or, ast.binary_expr(&tree, iteration.iterable).operator)
try testing.expect(ast.if_stmt(&tree, ast.block_statement(&tree, body, 3)).capture != null)
try testing.expect(ast.while_stmt(&tree, ast.block_statement(&tree, body, 4)).capture != null)
try testing.expect(ast.for_stmt(&tree, ast.block_statement(&tree, body, 5)).capture != null)
}