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
+6 -2
View File
@@ -160,9 +160,13 @@ callback signatures use `@proc(value i16) i16`, including grouped parameter name
`name = expression` updates a local `:=` binding. `::` bindings, parameters, named functions, builtin types and globals are not assignment targets. the established binding type is preserved: assignment inputs do not widen or infer that type, though numeric literals can adopt its float context. function-valued locals accept replacement functions with the same parameter and result types.
compound updates `+= -= *= /= &= |= xor= <<= >>= <<|=` preserve the target type and ordinary writable-place checks. the target address and old value are evaluated once before the right operand, then the result is stored through that same address; `/=` is float-only and `xor=` must be contiguous. field/index/pointer targets, cleanup bodies, ordinary compile-time calls and type-producing locals use the same checked operations.
integer `~`, `&`, `xor` and `|` preserve exact operand types, including distinct integer identities; incompatible fixed integer types remain invalid. shifts `<<`, `>>` and saturating `<<|` preserve the left type and accept unsigned counts independently of its width. ordinary shifts reject known out-of-range counts and trap for runtime counts at least the left bit width. left shifts discard high bits; signed right shifts extend the sign. saturating shifts accept any count, preserve zero and clamp nonzero values to the signed minimum/maximum or unsigned maximum. shifts bind below arithmetic; `&`, `xor` and `|` share one left-associative level below shifts and above comparisons. parenthesize bitwise-OR header expressions before capture lists. type-valued `A | B` remains error-set composition.
fixed arrays use `[N]T` for readonly elements and `[N]mut T` for mutable elements; literals use `[a, b]`, including empty, nested and multiline forms with trailing commas. `values [4]mut i64 := [1, 2, 3, 4]` permits `values[2] = 10`. writable elements require a mutable local binding and `mut` at every traversed array level; parameters and globals remain readonly. whole-array assignment only requires a mutable binding, but source and destination types must match recursively, including counts and element mutability. initializations, assignments, arguments and returns copy arrays by value, including nested rows.
array lengths currently support nonnegative integer literals, unary signs, `+`, `-`, `*` and package-level immutable integer constants; local constants and other compile-time expressions are not implemented. `[_]` infers a binding's array length from its initializer. element annotations require concrete supported value types: use `i64` or `f64` rather than the `int` or `float` constraints. an unannotated array literal has readonly elements.
array lengths support nonnegative integer literals, unary signs/complement, `+`, `-`, `*`, bitwise operators/shifts and package-level immutable integer constants; use `$()` for broader compile-time execution. `[_]` infers a binding's array length from its initializer. element annotations require concrete supported value types: use `i64` or `f64` rather than the `int` or `float` constraints. an unannotated array literal has readonly elements.
indices require `usize`; integer literals and flexible inferred values receive this context, while fixed signed integers do not convert implicitly. out-of-bounds access traps before loading or storing, including for empty arrays. reads evaluate the base before the index; indexed assignment evaluates its right-hand side before the destination, each once. leading nested assignment targets follow Bro's declaration ambiguity: write `(matrix[1])[0] = 9`, not `matrix[1][0] = 9`. [programs/array_sum.hon](programs/array_sum.hon) uses a `usize` range bound to index, mutate and sum an array; `./build/honey run programs/array_sum.hon` prints `17`.
@@ -269,4 +273,4 @@ IR construction owns derived layout metadata. `ir.add_array`, `ir.add_record` an
`interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned aggregates own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. aggregate descriptors also borrow the module; `ir.aggregate_item` projects a borrowed field/element while preserving zero-cell child shapes. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. compound assignment remains unimplemented.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure.
+6 -3
View File
@@ -61,6 +61,8 @@ runtime/constant values, contextual optional/fallible results and generated type
yielding value blocks, match arms and catch handlers copy results before scoped
cleanup and preserve enclosing function/loop transfers through both execution paths;
compile-time blocks also generate types, storage and defaults.
compound updates evaluate places once, and integer bitwise/truncating/saturating
shifts preserve widths and nominal types through runtime and compile-time execution.
the driver retains frontend storage and source provenance through
`@source/compilation`, with compilation-wide AST node identities.
file commands retain single-file behavior; directory commands compile the
@@ -127,11 +129,12 @@ test discovery and FFI remain outside these completed slices.
- [x] type-metadata reflection checkpoint: `typeinfo!(T)` uses the actual `@std/meta` ABI to classify supported types and expose array child/length, immediate distinct backing, record names/tuple/layout and enum/record fields through interpreter/native execution. readonly owned metadata collections retain exact nominal/type leaves, ordinal indices, composed enum identity, checked projections, by-value iteration/copies and compile-time procedure boundaries. ordinary/inline matches select runtime bodies; reflected fields drive `field!`, generics and generated storage/defaults. arity/type/import/ABI failures, match/capture validation, bounds, readonly updates, runtime escape rejection, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and ownership. existing `meta.EnumFieldStruct` and `enummap.EnumMap` generated storage are verified without replacement library code; full `enummap.init/get` is covered below, while general type-valued execution and broader constant materialization remain open.
- [x] enum-map acceptance checkpoint: existing `enummap.init/get` run through interpreter/native execution with sparse/aliased/composed enum keys, absent entries and owned aggregate values. contextual `.tag` and aggregate arguments reuse key/value types inferred from rigid generated-map arguments; unresolved generics and rigid conflicts still diagnose. `some!` constructs exact, single-evaluation optional payloads and preserves nested present-null values through compile-time results, cache keys and generated defaults. concrete mutable runtime locals initialize from `undefined`, including nested record/array updates, pointer access and by-value copies; ordinary compile-time procedures materialize fully initialized storage and reject incomplete snapshots. native undefined storage is poison, not zeroed or per-leaf checked. `get` now explicitly returns `null` on lookup fallthrough, preserving the ordinary nonvoid return contract.
- [x] array-list acceptance checkpoint: existing `std/arraylist` and `std/mem` initialize, reserve/grow, append/pop/clear and release ordinary/aggregate/zero-sized storage without replacement library code. nested generic inference canonicalizes generated types through pointers and derives exact slice `.ptr` types; rigid conflicts remain diagnostic. `minval!`/`maxval!` preserve integer types and checked `divtrunc!` traps on zero and signed overflow through interpreter/native execution. file-local helpers retain real `@hide:file` visibility, newline `else` preserves match default arms, and repeated `_` parameters remain sinks. generated C records, nominal opaque pointers and foreign callbacks load from existing `ffi/c`; unused unsupported signatures neither diagnose nor emit external symbols, while demanded unsupported ABI/types diagnose. interpreter fixed-capacity allocator checks cover failure without losing storage and deferred error cleanup; native `mem.c_allocator` checks cover aggregate growth and over-aligned allocation/reallocation at `-O0`/`-O2`. arbitrary interpreter FFI, variadic/aggregate C calls and broader library acceptance remain open.
- [x] boolean conditional-expression checkpoint: `if (guard) then_value else else_value` reuses canonical matches through interpreter/native execution. guards evaluate once and only the selected expression executes; nested conditionals bind `else` to the nearest conditional. exact result types, aggregate copy isolation, contextual optional/fallible channels, direct `$()` values, constant-input procedures, generic branch selection and nested generated type positions retain existing checked execution. missing `else`, nonboolean guards and incompatible runtime branch types diagnose. optional captures and yielding value blocks are covered below; compound updates and bitwise operators remain open before hash-map acceptance.
- [x] boolean conditional-expression checkpoint: `if (guard) then_value else else_value` reuses canonical matches through interpreter/native execution. guards evaluate once and only the selected expression executes; nested conditionals bind `else` to the nearest conditional. exact result types, aggregate copy isolation, contextual optional/fallible channels, direct `$()` values, constant-input procedures, generic branch selection and nested generated type positions retain existing checked execution. missing `else`, nonboolean guards and incompatible runtime branch types diagnose. optional captures, yielding value blocks, compound updates and bitwise operators are covered below.
- [x] optional conditional-expression checkpoint: `if value |payload| then_value else else_value` reuses canonical presence matches through interpreter/native execution. single-evaluation guards and lazy branches preserve exact immutable by-value captures, nested optional presence, aggregate isolation and runtime pointer/callback/slice/zero-sized payloads. contextual optional/fallible results, constant-input procedures, generated types/defaults and explicit compile-time subject selection retain checked execution and owned payload snapshots. nonoptional guards, capture scope/shadowing, malformed captures, missing `else`, incompatible branches and compile-time overflow retain diagnostics. yielding value blocks are covered below; broader constant materialization remains open.
- [x] yielding-block checkpoint: value blocks and `yield expression` produce exact/contextual results through interpreter/native execution, including conditional branches, match arms and catch handlers. nested value sources select the nearest target; ordinary blocks/loops preserve it. yielded aggregates snapshot before LIFO cleanup, successful yields skip `errdefer`, and return/error/break/continue retain enclosing targets. ordinary constants, generic branch specialization and type-producing blocks generate owned values, types, storage and defaults. missing operands/results, incompatible yields, capture mutation and outward cleanup transfers retain diagnostics and demand-driven traps; imported provenance and repeatable compilation are verified. labeled yields and value-producing loops remain outside this checkpoint.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), plus full existing-library acceptance for hash maps, broader `meta` operations and `io.print`. required expression-valued control flow, compound updates, formatting parsing and broader imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: compound updates, bitwise operators and multiline strings. boolean and optional-capture conditional expressions, yielding value blocks and `yield` are covered above. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
- [x] compound/bitwise checkpoint: arithmetic and bitwise compound updates evaluate mutable local/field/index/pointer places and their old values once before the RHS through interpreter/native execution. integer complement, and/or/xor, truncating left shifts, signed/unsigned right shifts and saturating left shifts preserve widths and distinct identity; unsigned counts retain independent types. known invalid counts diagnose even in untaken runtime branches; dynamic counts trap without native LLVM poison. checked arithmetic, float-only division, readonly diagnostics, scoped cleanup, ordinary constants, generated storage, generic branch selection and repeatable compilation retain existing semantics. type-valued `|` remains composition while runtime `|` and capture delimiters retain their grammar. full hash-map acceptance remains open.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), plus full existing-library acceptance for hash maps, broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
- [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support.
- [ ] self-hosting proof: bootstrap-built Honey builds the production compiler (excluding test-only imports); that executable rebuilds the compiler; the rebuilt compiler passes representative interpreter/native examples and CLI/error scenarios without Bro at runtime.
+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,
}
}
+27 -1
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)))
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, assignment.value)))
}
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: {
+93 -31
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{
span :: ast.Span{
start = ast.builder_span(state.builder, target).start,
end = end,
end = state.tokens[state.next_token - 1].span.end,
source = state.builder.source_id,
},
target,
value,
)
}
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)
)
}
+149
View File
@@ -11,6 +11,135 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = {
"distinct_bitwise_results_preserve_nominal_type": (
"""
Flags :: distinct u16
Signed :: distinct i8
exact proc(value Flags) Flags { return value }
main proc() [4]i16 {
flags Flags := 21
flags <<= u8(1)
flags xor= 3
flags &= 63
flags |= 2
signed Signed := -3
signed >>= u64(1)
return [i16(exact(flags)), i16(~Flags(65500)),
i16(signed), i16(Signed(-2) <<| 7)]
}
""", b"[43, 35, -2, -128]\n", None,
),
"compound_updates_evaluate_places_once_before_rhs": (
"""
Box :: struct { values [2]mut i16 }
place proc(box @mut Box, trace @mut i16) @mut i16 {
trace^ = trace^ * 10 + 1
return &box.values[0]
}
rhs proc(box @mut Box, trace @mut i16) i16 {
trace^ = trace^ * 10 + 2
box.values[0] = 100
return 2
}
index proc(trace @mut i16) usize {
trace^ = trace^ * 10 + 3
return 1
}
main proc() [4]i16 {
box Box := { values = [40, 6] }
trace i16 := 0
place(&box, &trace)^ += rhs(&box, &trace)
box.values[index(&trace)] *= 7
count i16 := 0
for (0..3) |_| { defer count += 1 }
return [box.values[0], box.values[1], trace, count]
}
""", b"[42, 42, 123, 3]\n", None,
),
"compound_bitwise_and_float_updates": (
"""
main proc() [4]i16 {
value u8 := 255
value &= 15
value xor= 3
value |= 1
value <<= 1
value >>= 1
value <<|= u64(1000)
number i16 := 20
number += 3
number -= 2
number *= 2
real f64 := 84
real /= 2
return [i16(value), number, i16(real), i16(~u8(213))]
}
""", b"[255, 42, 42, 42]\n", None,
),
"bitwise_precedence_and_type_composition": (
"""
A :: enum { first }
B :: enum { second }
Error :: A | B
fail proc() i16 ! Error { return .first }
main proc() [3]i16 {
return [i16(u8(1) | 2 xor 3 & 6), i16(u8(3) << (1 + 1)),
fail() catch |error| match error { .first: 42, .second: 0 }]
}
""", b"[0, 12, 42]\n", None,
),
"bitwise_constants_and_type_producer_updates": (
"""
Fixed proc() type {
counts [2]mut usize := [1, 2]
counts[0] <<= 2
counts[1] |= 1
total usize := counts[0]
total += counts[1]
return struct { values [total]i16, answer i16 = i16(~u8(213)) }
}
compute proc() u8 {
value u8 := 21
value <<= 1
return value
}
choose proc($value u8) i16 {
if ((value & 3) == 2) return 42
else return missing_member(value)
}
main proc() [4]i16 {
generated Fixed() :: { values = [0, 0, 0, 0, 0, 0, 0] }
array [(usize(1) << 2)]i16 :: [0, 0, 0, 0]
return [generated.answer, i16($(compute())), choose($(u8(21) << 1)),
i16(generated.values.len + array.len)]
}
missing_member proc(value u8) bool { return true }
""", b"[42, 42, 42, 11]\n", None,
),
"compound_signed_overflow": (
"main proc() i8 { value i8 := 127\nvalue += 1\nreturn value }",
b"", b"integer arithmetic overflow",
),
"compound_readonly_place": (
"main proc() i16 { values [1]i16 := [40]\nvalues[0] += 2\nreturn 0 }",
b"", b"indexed assignment requires a writable array element",
),
"bitwise_float_rejected": (
"main proc() f64 { return f64(1) & 2 }",
b"", b"operator requires compatible operand types",
),
"shift_signed_count_rejected": (
"main proc() i16 { return i16(1) << i16(2) }",
b"", b"operator requires compatible operand types",
),
"shift_known_count_rejected_even_untaken": (
"main proc() i16 { if (false) { _ = u8(1) << 8 }\nreturn 42 }",
b"42\n", b"shift count must be smaller than the integer bit width",
),
"shift_runtime_count_traps": (
"shift proc(count u64) u8 { return u8(1) << count }\nmain proc() u8 { return shift(256) }",
b"", b"shift count must be smaller than the integer bit width",
),
"yielding_blocks_nest_and_copy_before_scoped_cleanup": (
"""
Item :: struct { values [1]mut i16 }
@@ -7304,6 +7433,26 @@ for integer_type, bits, signed in (
):
minimum = -(1 << (bits - 1)) if signed else 0
maximum = (1 << (bits - int(signed))) - 1
CASES[f"{integer_type}_bitwise_boundaries"] = (
f"""
shift proc(value {integer_type}, count u64) [4]{integer_type} {{
return [value << count, value >> count, value <<| count, ~value]
}}
main proc() [8]{integer_type} {{
high :: shift(maxval!({integer_type}), {bits - 1})
huge :: shift_zero({integer_type}(0), maxval!(u64))
return [high[0], high[1], high[2], high[3],
{integer_type}(1) <<| maxval!(u64), huge,
minval!({integer_type}) <<| maxval!(u64),
minval!({integer_type}) >> u8({bits - 1})]
}}
shift_zero proc(value {integer_type}, count u64) {integer_type} {{ return value <<| count }}
""",
(f"[{- (1 << (bits - 1)) if signed else 1 << (bits - 1)}, "
f"{0 if signed else 1}, {maximum}, {minimum}, {maximum}, 0, {minimum}, "
f"{-1 if signed else 0}]\n").encode(),
None,
)
CASES[f"{integer_type}_limits_and_truncating_division"] = (
f"""
identity proc(value {integer_type}) {integer_type} {{ return value }}