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
+19 -5
View File
@@ -2,8 +2,8 @@
#! * type ..... -> type_sum ['!' type]; type_sum -> type_atom {'|' type_atom}; type_atom includes '(' type ')', identifier {'.' member | '(' [expr {',' expr} [',']] ')' | '[' expr ']'}, '?' type_atom, ('@' | '*') ['mut'] type_atom, ['@'] 'proc' '(' parameters ')' type, '[' (expr | '_') ']' ['mut'] type_atom, '[' [';' expr] ']' ['mut'] type_atom, record/enum/union declarations, and ('distinct' | 'alias') type.
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false' | 'null'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&' | 'try') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' | expr 'catch' ['|' identifier '|'] (expr | block)
#! * op ....... -> '+' | '-' | '*' | '/' | '&' | 'xor' | '|' | '<<' | '>>' | '<<|' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '~' | '&' | 'try') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' | expr 'catch' ['|' identifier '|'] (expr | block)
#! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string
#! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently.
#! '$(' expr ')' is an explicit compile-time expression with primary-expression precedence.
@@ -14,9 +14,11 @@
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated.
#! enum members and tagged-union variants accept keyword names; member names do not enter lexical scopes.
#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr '=' expr | 'return' [expr] | 'yield' expr | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | ['inline'] 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
#! braceless guards/iterables require parentheses unless they are calls, explicit comptime expressions or optional captures; else binds to the nearest if.
#! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -/!/&/try higher than multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges/catch. type composition | binds tighter than fallible !; pointer/optional/array prefixes bind to type atoms. compound range endpoints require their own parentheses.
#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr assignment_op expr | 'return' [expr] | 'yield' expr | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | ['inline'] 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
#! assignment_op is '=' or a contiguous compound: '+=', '-=', '*=', '/=', '&=', '|=', 'xor=', '<<=', '>>=', '<<|='.
#! braceless guards/iterables require parentheses unless they are calls, explicit comptime expressions or optional captures; else binds to the nearest if. bitwise-OR headers require parentheses before captures.
#! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -, !, ~, &, try higher than multiplication/division, addition/subtraction, shifts << / >> / <<|, bitwise &/xor/|, relational comparisons, equality, and, or, ranges/catch. type composition | binds tighter than fallible !; pointer/optional/array prefixes bind to type atoms. compound range endpoints require their own parentheses.
#! compound assignments share their target with the canonical binary value's left operand; lowering evaluates the place and loads its old value once before the RHS.
#! binary operators associate left; unary operators associate right.
import "@std/debug"
@@ -118,6 +120,7 @@ Mutability :: enum {
UnaryOperator :: enum {
negate
logical_not
bit_not
address_of
dereference
unwrap
@@ -126,6 +129,8 @@ UnaryOperator :: enum {
BinaryOperator :: enum {
add, subtract
multiply, divide
bit_and, bit_or, bit_xor
shift_left, shift_right, shift_left_saturating
equal, not_equal
less, less_equal
greater, greater_equal
@@ -214,6 +219,7 @@ NodeData :: union {
@hide:file variadic_flag u8 :: 0b0100
@hide:file opaque_flag u8 :: 0b1000
@hide:file optional_match_flag u8 :: 0b00010000
@hide:file compound_assignment_flag u8 :: 0b00100000
@hide:file
Node :: struct {
@@ -405,6 +411,7 @@ AssignmentView :: struct {
span Span
target NodeId
value NodeId
compound bool = false
}
WhileView :: struct {
@@ -1023,6 +1030,12 @@ add_assignment_stmt proc(
}, span)
}
set_assignment_compound proc(builder @mut Builder, id NodeId) void {
node :: &builder.storage.nodes.items[usize(id)]
debug.assert(node.kind == .stmt_assignment)
node.flags = node.flags | compound_assignment_flag
}
add_while_stmt proc(
builder @mut Builder,
span Span,
@@ -1416,6 +1429,7 @@ assignment_stmt proc(tree @Tree, id NodeId) AssignmentView {
span = get_span(&tree.storage, id),
target = node.main.node_id,
value = node.data0.node_id,
compound = (node.flags & compound_assignment_flag) != 0,
}
}
+28 -2
View File
@@ -94,8 +94,17 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
assignment :: ast.assignment_stmt(tree, statement)
try append_text(renderer, "assignment ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, assignment.target)))
try append_text(renderer, " = ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, assignment.value)))
value := assignment.value
if (assignment.compound) {
binary :: ast.binary_expr(tree, value)
try append_text(renderer, " ")
try append_text(renderer, assignment_operator_text(binary.operator))
try append_text(renderer, "= ")
value = binary.right
} else {
try append_text(renderer, " = ")
}
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
try append_text(renderer, "\n")
}
.while_stmt: {
@@ -151,3 +160,20 @@ render_statement proc(
append_text proc(renderer @mut Renderer, text []u8) void ! mem.AllocError {
for (text) |byte| try arraylist.append(&renderer.bytes, byte)
}
@hide:file
assignment_operator_text proc(operator ast.BinaryOperator) []u8 {
return match operator {
.add: "+"
.subtract: "-"
.multiply: "*"
.divide: "/"
.bit_and: "&"
.bit_or: "|"
.bit_xor: "xor"
.shift_left: "<<"
.shift_right: ">>"
.shift_left_saturating: "<<|"
else: unreachable
}
}
+47
View File
@@ -847,3 +847,50 @@ yielding_blocks_recompile_owned_constants_and_generated_types test {
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
}
compound_updates_and_type_composition_recompile_without_retaining_rewrites test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
modules [2]mut ir.Module := undefined
{
session := init(mem.c_allocator)
defer deinit(&session)
root :: try add_package(&session)
bytes := try owned_text(
`A :: enum { first }
`B :: enum { second }
`Errors :: A | B
`Box proc() type {
` counts [1]mut usize := [1]
` counts[0] <<= 1
` return struct { values [counts[0]]i16 }
`}
`compute proc() u8 {
` value u8 := 21
` value <<= 1
` return value
`}
`main proc() i16 {
` box Box() :: { values = [40, 2] }
` answer i16 := i16($(compute()))
` answer xor= box.values[0] + box.values[1]
` return answer + 42
`}
,
)
defer arraylist.deinit(&bytes)
_ = try add_source(&session, root, "operators.hon", &bytes, &diagnostic_store)
modules[0] = try compile(&session, root, &diagnostic_store)
errdefer ir.deinit(&modules[0])
modules[1] = try compile(&session, root, &diagnostic_store)
}
defer ir.deinit(&modules[0])
defer ir.deinit(&modules[1])
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
for modules |module| {
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
}
+2
View File
@@ -126,6 +126,7 @@ Code :: enum {
missing_metadata_import
invalid_metadata_abi
invalid_undefined
invalid_shift_count
}
Diagnostic :: struct {
@@ -179,6 +180,7 @@ messages :: $enummap.init(Code, []u8, {
type_mismatch = "incompatible value type",
invalid_operand = "operator requires compatible operand types",
integer_division = "division requires float operands",
invalid_shift_count = "shift count must be smaller than the integer bit width",
not_callable = "value is not callable",
argument_count_mismatch = "argument count does not match the function signature",
cannot_infer_type = "cannot infer a type without a type anchor",
+63
View File
@@ -587,6 +587,29 @@ execute proc(
operand :: frames.items[frame_index].values[usize(instruction.left)]
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = !operand.data.boolean } }
}
.bit_not: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
mask :: maxval!(u64) >> u64(64 - ir.integer_bits(instruction.type))
value = integer_from_bits(instruction.type, operand.data.unsigned xor mask)
}
.bit_and, .bit_or, .bit_xor: {
left :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned
right :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
bits u64 :: match instruction.op {
.bit_and: left & right
.bit_or: left | right
.bit_xor: left xor right
else: unreachable
}
value = integer_from_bits(instruction.type, bits)
}
.shift_left, .shift_right, .shift_left_saturating: {
left :: frames.items[frame_index].values[usize(instruction.left)]
count :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
if (instruction.op != .shift_left_saturating and count >= u64(ir.integer_bits(instruction.type)))
return try runtime_trap(diagnostic_store, .invalid_shift_count, instruction.span)
value = shift_integer(instruction.op, left, count)
}
.add, .subtract, .multiply, .divide: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
@@ -901,6 +924,46 @@ runtime_trap proc(
return Outcome{ trap = try diagnostics.add(diagnostic_store, code, span) }
}
@hide:file
integer_from_bits proc(type ir.Type, bits u64) ir.Value {
data := ir.ValueData{ unsigned = bits }
match type {
.i8: data = ir.ValueData{ integer = i64(ptrcast!(@i8, &bits)^) }
.i16: data = ir.ValueData{ integer = i64(ptrcast!(@i16, &bits)^) }
.i32: data = ir.ValueData{ integer = i64(ptrcast!(@i32, &bits)^) }
.int, .isize: data = ir.ValueData{ integer = ptrcast!(@i64, &bits)^ }
.u8: data = ir.ValueData{ unsigned = u64(ptrcast!(@u8, &bits)^) }
.u16: data = ir.ValueData{ unsigned = u64(ptrcast!(@u16, &bits)^) }
.u32: data = ir.ValueData{ unsigned = u64(ptrcast!(@u32, &bits)^) }
.u64, .usize: {}
else: unreachable
}
return ir.Value{ type = type, data = data }
}
@hide:file
shift_integer proc(op ir.Op, left ir.Value, count u64) ir.Value {
width :: u64(ir.integer_bits(left.type))
mask :: maxval!(u64) >> (64 - width)
bits :: left.data.unsigned & mask
signed :: !ir.unsigned_integer(left.type)
negative :: signed and left.data.integer < 0
if (op == .shift_left_saturating) {
if (bits == 0) return left
limit :: if (negative) 1 + (mask >> 1) else ir.integer_max(left.type)
magnitude :: if (negative) 1 + (bits xor mask) else bits
if (count >= width) return integer_from_bits(left.type, limit)
if (magnitude > (limit >> count)) return integer_from_bits(left.type, limit)
}
if (op == .shift_right) {
shifted := bits >> count
if (negative) shifted = shifted | (mask xor (mask >> count))
return integer_from_bits(left.type, shifted)
}
# discard high bits before shifting so the bootstrap's checked arithmetic cannot overflow.
return integer_from_bits(left.type, (bits & (mask >> count)) << count)
}
@hide:file
checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
minimum :: minval!(i64)
+8 -12
View File
@@ -155,22 +155,18 @@ Op :: enum {
slice_check
sentinel_check
optional_check
memory_copy
memory_set
memory_copy, memory_set
negate
convert
logical_not
add
subtract
multiply
divide
bit_not, bit_and, bit_or, bit_xor
shift_left, shift_right, shift_left_saturating
add, subtract
multiply, divide
call
equal
not_equal
less
less_equal
greater
greater_equal
equal, not_equal
less, less_equal
greater, greater_equal
}
Instruction :: struct {
+34 -3
View File
@@ -41,6 +41,7 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "false", .false },
{ "and", .and },
{ "or", .or },
{ "xor", .xor },
{ "mut", .mut },
{ "struct", .struct },
{ "c_struct", .c_struct },
@@ -107,12 +108,16 @@ scan proc(
if is_alpha(char) or char == '_' {
start :: cursor
result :: try scan_ident(start, program, pool)
compound :: result.kind == .xor and result.end < program.len and program[result.end] == '='
extra :: if (compound) usize(1) else 0
end :: result.end + extra
kind :: if (compound) TokenKind.xor_equal else result.kind
try add_token(state, Token{
span = make_span(source, start, result.end),
kind = result.kind,
span = make_span(source, start, end),
kind = kind,
data = TokenData{ str_id = result.str_id },
})
cursor = result.end
cursor = end
continue
}
@@ -281,12 +286,37 @@ scan proc(
continue
}
if (
(char == '<' or char == '>') and cursor + 1 < program.len
and program[cursor + 1] == char
) {
saturating :: char == '<' and cursor + 2 < program.len and program[cursor + 2] == '|'
width :: if (saturating) usize(3) else 2
compound :: cursor + width < program.len and program[cursor + width] == '='
kind TokenKind := .shift_right
if (saturating) {
kind = if (compound) .shift_left_saturating_equal else .shift_left_saturating
} else if (char == '<') {
kind = if (compound) .shift_left_equal else .shift_left
} else if (compound) kind = .shift_right_equal
extra :: if (compound) usize(1) else 0
end :: cursor + width + extra
try add_token(state, Token{
span = make_span(source, cursor, end),
kind = kind,
})
cursor = end
continue
}
if (cursor + 1 < program.len and program[cursor + 1] == '=') {
comparison ?TokenKind :: match char {
'=': .equal_equal
'!': .not_equal
'<': .less_equal
'>': .greater_equal
'&': .ampersand_equal
'|': .pipe_equal
else: null
}
if (comparison) |value| {
@@ -302,6 +332,7 @@ scan proc(
kind ?TokenKind :: match char {
'=': .equal
'!': .bang
'~': .tilde
'<': .less
'>': .greater
',': .comma
+28 -2
View File
@@ -11,7 +11,7 @@ reports_invalid_input_and_keeps_scanning test {
state := init(mem.c_allocator)
defer deinit(&state)
try scan(&state, "if name # comment\n~1. 2 3.5 \"hi\"", &pool, &diagnostic_store)
try scan(&state, "if name # comment\n%1. 2 3.5 \"hi\"", &pool, &diagnostic_store)
try testing.expect_equal(9, state.tokens.items.len)
try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind)
@@ -266,7 +266,7 @@ scans_import_keywords_and_preserves_source_in_tokens_and_diagnostics test {
state := init(mem.c_allocator)
defer deinit(&state)
state.source_id = diagnostics.SourceId(7)
try scan(&state, "import test ~\n1.\n\"unfinished", &pool, &diagnostic_store)
try scan(&state, "import test %\n1.\n\"unfinished", &pool, &diagnostic_store)
try testing.expect_equal(TokenKind.import, state.tokens.items[0].kind)
try testing.expect_equal(TokenKind.test, state.tokens.items[1].kind)
for (state.tokens.items) |token| try testing.expect_equal(state.source_id, token.span.source)
@@ -290,3 +290,29 @@ reserves_yield_without_splitting_identifiers test {
try testing.expect_equal("yield", strpool.get_str(&pool, state.tokens.items[0].data.str_id)?)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
scans_bitwise_operators_and_contiguous_compounds test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
state := init(mem.c_allocator)
defer deinit(&state)
source :: "~ & | xor << >> <<| += -= *= /= &= |= xor= <<= >>= <<|= xor_flag xor = << |= <<| ="
try scan(&state, source, &pool, &diagnostic_store)
expected [25]TokenKind :: [
.tilde, .ampersand, .pipe, .xor, .shift_left, .shift_right, .shift_left_saturating,
.plus_equal, .minus_equal, .star_equal, .slash_equal,
.ampersand_equal, .pipe_equal, .xor_equal,
.shift_left_equal, .shift_right_equal, .shift_left_saturating_equal,
.ident, .xor, .equal, .shift_left, .pipe_equal, .shift_left_saturating, .equal, .eof,
]
try testing.expect_equal(expected.len, state.tokens.items.len)
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
xor_span :: state.tokens.items[13].span
try testing.expect_equal("xor=", source[usize(xor_span.start)..usize(xor_span.end)])
saturating_span :: state.tokens.items[16].span
try testing.expect_equal("<<|=", source[usize(saturating_span.start)..usize(saturating_span.end)])
try testing.expect_equal("xor_flag", strpool.get_str(&pool, state.tokens.items[17].data.str_id)?)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
+7
View File
@@ -29,6 +29,7 @@ TokenKind :: enum {
defer, errdefer
true, false
and, or
xor
struct, c_struct
opaque
enum, union
@@ -56,6 +57,10 @@ TokenKind :: enum {
plus, minus
star, slash
# bitwise
tilde
shift_left, shift_right, shift_left_saturating
range, range_inclusive, ellipsis
at, ampersand, caret, dollar
question, semicolon
@@ -67,6 +72,8 @@ TokenKind :: enum {
# assignment & arithmetic
plus_equal, minus_equal
star_equal, slash_equal
ampersand_equal, pipe_equal, xor_equal
shift_left_equal, shift_right_equal, shift_left_saturating_equal
# delimiters
open_paren, close_paren
+68
View File
@@ -304,6 +304,22 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
operand :: try load(state, instruction.left)
try io.print(" %r{} = xor i1 %r{}, true\n", output, {result, operand})
}
.bit_not: {
operand :: try load(state, instruction.left)
try io.print(" %r{} = xor i{} %r{}, -1\n", output, {result, ir.integer_bits(instruction.type), operand})
}
.bit_and, .bit_or, .bit_xor: {
left :: try load(state, instruction.left)
right :: try load(state, instruction.right)
opcode []u8 :: match instruction.op {
.bit_and: "and"
.bit_or: "or"
.bit_xor: "xor"
else: unreachable
}
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {result, opcode, ir.integer_bits(instruction.type), left, right})
}
.shift_left, .shift_right, .shift_left_saturating: result = try emit_shift(state, instruction)
.add, .subtract, .multiply, .divide: {
left :: try load(state, instruction.left)
right :: try load(state, instruction.right)
@@ -388,6 +404,58 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
try store(state, instruction.destination, result)
}
@hide:file
emit_shift proc(state @mut State, instruction ir.Instruction) usize ! io.WriteError {
output :: state.output
left :: try load(state, instruction.left)
right :: try load(state, instruction.right)
bits :: ir.integer_bits(instruction.type)
count_bits :: ir.integer_bits(state.values[usize(instruction.right)].kind)
invalid :: fresh(state)
try io.print(" %r{} = icmp uge i{} %r{}, {}\n", output, {invalid, count_bits, right, bits})
count := right
if (instruction.op == .shift_left_saturating) {
# keep every evaluated LLVM shift in range, including the discarded saturation path.
count = fresh(state)
try io.print(" %r{} = select i1 %r{}, i{} 0, i{} %r{}\n", output, {count, invalid, count_bits, count_bits, right})
} else {
try check_failure(state, invalid, .invalid_shift_count, instruction.span)
}
if (count_bits != bits) {
converted :: fresh(state)
conversion []u8 :: if (count_bits < bits) "zext" else "trunc"
try io.print(" %r{} = {s} i{} %r{} to i{}\n", output, {converted, conversion, count_bits, count, bits})
count = converted
}
shifted :: fresh(state)
signed :: !ir.unsigned_integer(instruction.type)
right_opcode []u8 :: if (signed) "ashr" else "lshr"
opcode []u8 :: if (instruction.op == .shift_right) right_opcode else "shl"
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {shifted, opcode, bits, left, count})
if (instruction.op != .shift_left_saturating) return shifted
restored :: fresh(state)
lost :: fresh(state)
overflow :: fresh(state)
nonzero :: fresh(state)
saturate :: fresh(state)
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {restored, right_opcode, bits, shifted, count})
try io.print(" %r{} = icmp ne i{} %r{}, %r{}\n", output, {lost, bits, restored, left})
try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {overflow, invalid, lost})
try io.print(" %r{} = icmp ne i{} %r{}, 0\n", output, {nonzero, bits, left})
try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {saturate, overflow, nonzero})
limit :: fresh(state)
if (signed) {
negative :: fresh(state)
try io.print(" %r{} = icmp slt i{} %r{}, 0\n", output, {negative, bits, left})
try io.print(" %r{} = select i1 %r{}, i{} {}, i{} {}\n", output, {limit, negative, bits, ir.integer_min(instruction.type), bits, ir.integer_max(instruction.type)})
} else {
try io.print(" %r{} = or i{} 0, {}\n", output, {limit, bits, ir.integer_max(instruction.type)})
}
result :: fresh(state)
try io.print(" %r{} = select i1 %r{}, i{} %r{}, i{} %r{}\n", output, {result, saturate, bits, limit, bits, shifted})
return result
}
#! compares the complete sentinel value, including nested aggregate fields.
@hide:file
sentinel_difference proc(state @mut State, type Type, actual, expected usize) usize ! io.WriteError {
+3 -1
View File
@@ -64,10 +64,11 @@ emit proc(
try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, false)
try io.write_all(output, "\n")
}
memory_codes [7]diagnostics.Code :: [
memory_codes [_]diagnostics.Code :: [
.null_unwrap, .invalid_slice_bounds, .sentinel_mismatch,
.overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access,
.division_by_zero,
.invalid_shift_count,
]
for (memory_codes) |code| {
try io.print("@memory_{s} = private constant ", output, {memory_symbol(code)})
@@ -504,6 +505,7 @@ memory_symbol proc(code diagnostics.Code) []u8 {
.unsupported_foreign_call: "unsupported_foreign_call"
.invalid_pointer_access: "invalid_pointer_access"
.division_by_zero: "division_by_zero"
.invalid_shift_count: "invalid_shift_count"
else: unreachable
}
}
+55 -13
View File
@@ -927,6 +927,36 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(tree, id)
if (assignment.compound) {
if (trap_type(state, id)) return
expression :: ast.binary_expr(tree, assignment.value)
address :: try lower_place(state, assignment.target)
if (state.terminated) return
checked :: typecheck.type_of(state.checked, assignment.target)
type :: lower_type(state, checked)
old :: try emit(state, ir.Instruction{
op = .load_address, type = type, left = address,
array_type = try array_shape(state, checked), span = ast.node_span(tree, assignment.target),
})
right :: try lower_expression(state, expression.right)
if (state.terminated) return
value :: if (expression.operator == .add and type == .pointer) {
pointer :: typecheck.pointer_type(state.checked, checked.pointer_type)
yield try emit(state, ir.Instruction{
op = .pointer_offset, type = type, left = old, right = right,
pointee = lower_type(state, pointer.element),
array_type = try array_shape(state, pointer.element), span = ast.node_span(tree, id),
})
} else try emit(state, ir.Instruction{
op = binary_op(expression.operator), type = type, left = old, right = right,
span = ast.node_span(tree, id),
})
_ = try emit(state, ir.Instruction{
op = .store_address, type = .void, left = address, right = value,
span = ast.node_span(tree, assignment.target),
})
return
}
if (
ast.expr_kind(tree, assignment.target) == .index
or ast.expr_kind(tree, assignment.target) == .unary
@@ -1016,6 +1046,29 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
}
}
@hide:file
binary_op proc(operator ast.BinaryOperator) ir.Op {
return match operator {
.add: .add
.subtract: .subtract
.multiply: .multiply
.divide: .divide
.bit_and: .bit_and
.bit_or: .bit_or
.bit_xor: .bit_xor
.shift_left: .shift_left
.shift_right: .shift_right
.shift_left_saturating: .shift_left_saturating
.equal: .equal
.not_equal: .not_equal
.less: .less
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
.logical_and, .logical_or: unreachable
}
}
@hide:file
lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.binary_expr(state.tree, id)
@@ -2199,6 +2252,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
op ir.Op :: match expression.operator {
.negate: .negate
.logical_not: .logical_not
.bit_not: .bit_not
.address_of, .dereference, .unwrap: unreachable
}
return try emit(state, ir.Instruction{ op = op, type = type, left = operand, span = span })
@@ -2231,19 +2285,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
array_type = try array_shape(state, pointer.element), span = span,
})
}
op ir.Op :: match expression.operator {
.add: .add
.subtract: .subtract
.multiply: .multiply
.divide: .divide
.equal: .equal
.not_equal: .not_equal
.less: .less
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
.logical_and, .logical_or: unreachable
}
op :: binary_op(expression.operator)
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
.call: {
+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)
}
+105 -7
View File
@@ -438,7 +438,7 @@ check_elaborated proc(
match ast.expr_kind(tree, id) {
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) try require_numeric(&state, id, expression.operand)
if (expression.operator == .negate or expression.operator == .bit_not) try require_numeric(&state, id, expression.operand)
}
.binary: {
expression :: ast.binary_expr(tree, id)
@@ -464,7 +464,7 @@ check_elaborated proc(
match ast.expr_kind(tree, id) {
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) try numeric_operand(&state, id, expression.operand)
if (expression.operator == .negate or expression.operator == .bit_not) try numeric_operand(&state, id, expression.operand)
}
.binary: {
expression :: ast.binary_expr(tree, id)
@@ -473,7 +473,11 @@ check_elaborated proc(
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
} else {
try numeric_operand(&state, id, expression.left)
try numeric_operand(&state, id, expression.right)
if (shift(expression.operator)) {
count_group :: root(&state, usize(expression.right))
if (state.cells[count_group].type.kind == .none and state.cells[count_group].demand == .none)
try impose(&state, expression.right, .usize, .invalid_operand, expression.span)
} else try numeric_operand(&state, id, expression.right)
if (expression.operator == .divide) {
try impose(&state, id, .float, .integer_division, expression.span)
}
@@ -1370,10 +1374,10 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) state.cells[usize(id)].producer = id
if (expression.operator == .negate or expression.operator == .bit_not) state.cells[usize(id)].producer = id
if (expression.operator == .logical_not) {
state.cells[usize(id)].type = Type{ kind = .bool }
} else if (expression.operator == .negate) {
} else if (expression.operator == .negate or expression.operator == .bit_not) {
state.cells[usize(id)].numeric = true
}
operand :: expression.operand
@@ -1570,6 +1574,27 @@ logical proc(operator ast.BinaryOperator) bool {
return operator == .logical_and or operator == .logical_or
}
@hide:file
shift proc(operator ast.BinaryOperator) bool {
return operator == .shift_left or operator == .shift_right or operator == .shift_left_saturating
}
@hide:file
bitwise proc(operator ast.BinaryOperator) bool {
return operator == .bit_and or operator == .bit_or or operator == .bit_xor or shift(operator)
}
@hide:file
integer_width proc(kind TypeKind) usize {
return match kind {
.i8, .u8: 8
.i16, .u16: 16
.i32, .u32: 32
.int, .isize, .u64, .usize: 64
else: 0
}
}
@hide:file
pointer_addition proc(state @mut State, id ast.NodeId) bool {
expression :: ast.binary_expr(state.tree, id)
@@ -2153,7 +2178,9 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
return
}
operand :: state.types[usize(expression.operand)]
if (operand.kind != .invalid and (!numeric_kind(operand.kind) or unsigned_kind(operand.kind))) try fail(state, id, .invalid_operand, ast.node_span(tree, id))
if (expression.operator == .bit_not) {
if (operand.kind != .invalid and !integer_kind(operand.kind)) try fail(state, id, .invalid_operand, expression.span)
} else if (operand.kind != .invalid and (!numeric_kind(operand.kind) or unsigned_kind(operand.kind))) try fail(state, id, .invalid_operand, ast.node_span(tree, id))
}
.binary: {
expression :: ast.binary_expr(tree, id)
@@ -2165,6 +2192,21 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
left :: state.types[usize(expression.left)]
right :: state.types[usize(expression.right)]
if (left.kind == .invalid or right.kind == .invalid) return
if (shift(expression.operator)) {
if (!integer_kind(left.kind) or !unsigned_kind(right.kind)) {
try fail(state, id, .invalid_operand, expression.span)
} else if (expression.operator != .shift_left_saturating) {
if (evaluate_integer(state, expression.right, true)) |count| {
if (count.negative or count.magnitude >= u64(integer_width(left.kind)))
try fail(state, id, .invalid_shift_count, expression.span)
}
}
return
}
if (bitwise(expression.operator) and (!integer_kind(left.kind) or !integer_kind(right.kind))) {
try fail(state, id, .invalid_operand, expression.span)
return
}
if (pointer_addition(state, id)) {
if (right.kind != .usize or right.nominal != ast.NodeId(maxval!(u32))) try fail(state, id, .invalid_operand, expression.span)
return
@@ -2378,6 +2420,53 @@ constant_integer proc(state @mut State, id ast.NodeId) ?ConstantInteger {
return evaluate_integer(state, id, false)
}
@hide:file
constant_bits proc(value ConstantInteger, mask u64) u64 {
if (value.negative) return (value.magnitude - 1) xor mask
return value.magnitude & mask
}
@hide:file
constant_from_bits proc(bits u64, kind TypeKind) ConstantInteger {
mask :: maxval!(u64) >> u64(64 - integer_width(kind))
negative :: !unsigned_kind(kind) and bits > (mask >> 1)
magnitude :: if (negative) 1 + (bits xor mask) else bits
return ConstantInteger{
magnitude = magnitude,
negative = negative,
}
}
@hide:file
constant_bitwise proc(state @mut State, id ast.NodeId, left, right ConstantInteger) ?ConstantInteger {
view :: ast.binary_expr(state.tree, id)
family :: bound_family(state, id)
kind :: if (family == .none) TypeKind.int else family
width :: integer_width(kind)
if (width == 0 or !integer_fits(left, kind)) return null
mask :: maxval!(u64) >> u64(64 - width)
a :: constant_bits(left, mask)
b :: constant_bits(right, mask)
if (view.operator == .bit_and) return constant_from_bits(a & b, kind)
if (view.operator == .bit_or) return constant_from_bits(a | b, kind)
if (view.operator == .bit_xor) return constant_from_bits(a xor b, kind)
if (right.negative) return null
count :: right.magnitude
if (view.operator == .shift_left_saturating) {
signed_limit :: if (left.negative) 1 + (mask >> 1) else mask >> 1
limit :: if (unsigned_kind(kind)) mask else signed_limit
if (left.magnitude == 0) return left
if (count >= u64(width) or left.magnitude > (limit >> count))
return ConstantInteger{ magnitude = limit, negative = left.negative }
} else if (count >= u64(width)) return null
if (view.operator == .shift_right) {
shifted :: a >> count
bits :: if (left.negative) shifted | (mask xor (mask >> count)) else shifted
return constant_from_bits(bits, kind)
}
return constant_from_bits((a << count) & mask, kind)
}
@hide:file
evaluate_integer proc(state @mut State, id ast.NodeId, allow_local bool) ?ConstantInteger {
if (state.cells[usize(id)].constant_visiting) return null
@@ -2431,8 +2520,15 @@ evaluate_integer proc(state @mut State, id ast.NodeId, allow_local bool) ?Consta
}
.unary: {
view :: ast.unary_expr(state.tree, id)
if (view.operator != .negate) return null
if (view.operator != .negate and view.operator != .bit_not) return null
if (evaluate_integer(state, view.operand, allow_local)) |value| {
if (view.operator == .bit_not) {
family :: bound_family(state, id)
kind :: if (family == .none) TypeKind.int else family
if (!integer_kind(kind) or !integer_fits(value, kind)) return null
mask :: maxval!(u64) >> u64(64 - integer_width(kind))
return constant_from_bits(constant_bits(value, mask) xor mask, kind)
}
return ConstantInteger{ magnitude = value.magnitude, negative = value.magnitude != 0 and !value.negative }
}
return null
@@ -2443,6 +2539,7 @@ evaluate_integer proc(state @mut State, id ast.NodeId, allow_local bool) ?Consta
right := ConstantInteger{ magnitude = 0 }
if (evaluate_integer(state, view.left, allow_local)) |value| left = value else return null
if (evaluate_integer(state, view.right, allow_local)) |value| right = value else return null
if (bitwise(view.operator)) return constant_bitwise(state, id, left, right)
if (view.operator == .multiply) {
if (right.magnitude != 0 and left.magnitude > divtrunc!(maxval!(u64), right.magnitude)) return null
magnitude :: left.magnitude * right.magnitude
@@ -3422,6 +3519,7 @@ bound_family proc(state @mut State, id ast.NodeId) TypeKind {
if (ast.expr_kind(state.tree, producer) == .unary) return bound_family(state, ast.unary_expr(state.tree, producer).operand)
view :: ast.binary_expr(state.tree, producer)
left :: bound_family(state, view.left)
if (shift(view.operator)) return left
right :: bound_family(state, view.right)
if (left == .float or right == .float) return .float
if (integer_kind(left)) return left
+1 -1
View File
@@ -718,7 +718,7 @@ resolves_every_array_child_error_and_preserves_lexer_diagnostics test {
` return
` data [missing_count]MissingElement :: [absent, missing_element]
` (missing_base[missing_index])[missing_outer] = missing_rhs
` malformed [~]i64 :: [~]
` malformed [%]i64 :: [%]
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
+32 -3
View File
@@ -125,6 +125,8 @@ elaborate proc(
if (class == .expression
and (ast.expr_kind(&original, id) == .comptime_expr or ast.expr_kind(&original, id) == .block_expr))
needed = true
if (class == .expression and ast.expr_kind(&original, id) == .binary
and ast.binary_expr(&original, id).operator == .bit_or) needed = true
if (class == .statement and ast.stmt_kind(&original, id) == .for_stmt
and ast.for_stmt(&original, id).is_inline) needed = true
if (class == .match_arm and ast.match_arm_view(&original, id).is_inline) needed = true
@@ -451,6 +453,14 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .binary) {
view :: ast.binary_expr(&state.tree, id)
if (view.operator == .bit_or
and (is_type(state, try expression_type(state, view.left, 0))
or is_type(state, try expression_type(state, view.right, 0)))) {
composed :: try ast.add_binary_type(state.builder, view.span, view.left, view.right, false)
try replace(state, id, composed)
try refresh(state)
return
}
if (
(view.operator == .equal or view.operator == .not_equal)
and branch_constant(state, view.left, 0) and branch_constant(state, view.right, 0)
@@ -1502,7 +1512,7 @@ branch_constant proc(state @State, id ast.NodeId, depth usize) bool {
.unary: {
view :: ast.unary_expr(&state.tree, id)
return (
(view.operator == .negate or view.operator == .logical_not)
(view.operator == .negate or view.operator == .logical_not or view.operator == .bit_not)
and branch_constant(state, view.operand, depth + 1)
)
}
@@ -1955,6 +1965,15 @@ contextual_literal proc(state @State, id ast.NodeId) bool {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .builtin_call)
return mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name), "some")
if (kind == .unary) return ast.unary_expr(&state.tree, id).operator == .bit_not and flexible(state, id)
if (kind == .binary) {
operator :: ast.binary_expr(&state.tree, id).operator
return (
(operator == .bit_and or operator == .bit_or or operator == .bit_xor
or operator == .shift_left or operator == .shift_right or operator == .shift_left_saturating)
and flexible(state, id)
)
}
return kind == .record_literal or kind == .array_literal or kind == .enum_literal or kind == .block_expr
}
@@ -3963,6 +3982,7 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution,
if (target != assignment.target) {
normalized :: try ast.add_assignment_stmt(state.builder,
ast.node_span(&state.tree, statement), target, assignment.value)
if (assignment.compound) ast.set_assignment_compound(state.builder, normalized)
try refresh(state)
# synthesized places still need local substitution in snapshots.
previous :: execution.mapping.len
@@ -4442,6 +4462,8 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
.alias_expr: return try evaluate(state, ast.type_operand(&state.tree, id))
.unary: {
view :: ast.unary_expr(&state.tree, id)
if (view.operator == .bit_not)
return try evaluate_ir(state, id, try expression_type(state, id, 0))
value := try evaluate(state, view.operand)
if (view.operator == .negate and value.kind == .integer) value.negative = value.magnitude != 0 and !value.negative
else if (view.operator == .negate and value.kind == .real) value.magnitude = value.magnitude xor (u64(1) << 63)
@@ -4484,6 +4506,9 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error {
view :: ast.binary_expr(&state.tree, id)
if (view.operator == .divide) return try evaluate_ir(state, id, try builtin(state, id, .float))
if (view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor
or view.operator == .shift_left or view.operator == .shift_right or view.operator == .shift_left_saturating)
return try evaluate_ir(state, id, try expression_type(state, id, 0))
left :: try evaluate(state, view.left)
if (left.kind == .invalid) return Value{}
if (left.kind == .boolean) {
@@ -4978,6 +5003,8 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal
or view.operator == .logical_and or view.operator == .logical_or
) return try builtin(state, id, .bool)
if ((view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor)
and flexible(state, view.left)) return try expression_type(state, view.right, depth + 1)
return try expression_type(state, view.left, depth + 1)
}
.selection: {
@@ -5326,12 +5353,14 @@ flexible proc(state @State, id ast.NodeId) bool {
if (kind == .literal_int or kind == .literal_float) return true
if (kind == .unary) {
view :: ast.unary_expr(&state.tree, id)
return view.operator == .negate and flexible(state, view.operand)
return (view.operator == .negate or view.operator == .bit_not) and flexible(state, view.operand)
}
if (kind == .binary) {
view :: ast.binary_expr(&state.tree, id)
return (
(view.operator == .add or view.operator == .subtract or view.operator == .multiply or view.operator == .divide)
(view.operator == .add or view.operator == .subtract or view.operator == .multiply or view.operator == .divide
or view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor
or view.operator == .shift_left or view.operator == .shift_right or view.operator == .shift_left_saturating)
and flexible(state, view.left) and flexible(state, view.right)
)
}