From d7e637dc3b58b2570f0138b77de4f381ccee71fc Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 30 Oct 2026 00:18:44 +0100 Subject: [PATCH] feat: support compound updates and bitwise operators --- README.md | 8 +- TODO.md | 9 +- source/ast/ast.hon | 24 +++- source/ast/renderer/renderer.hon | 30 ++++- source/compilation/compilation.test.hon | 47 ++++++++ source/diagnostics/diagnostics.hon | 2 + source/interpreter/interpreter.hon | 63 ++++++++++ source/ir/ir.hon | 20 ++-- source/lexer/scan.hon | 37 +++++- source/lexer/scan.test.hon | 30 ++++- source/lexer/token.hon | 7 ++ source/llvm/instructions.hon | 68 +++++++++++ source/llvm/llvm.hon | 4 +- source/lower/lower.hon | 68 ++++++++--- source/parser/parse.hon | 128 ++++++++++++++------ source/parser/parse.test.hon | 135 ++++++++++++++++++++- source/sema/check/check.hon | 112 ++++++++++++++++-- source/sema/resolve.test.hon | 2 +- source/sema/specialize/specialize.hon | 35 +++++- tests/native.py | 149 ++++++++++++++++++++++++ 20 files changed, 890 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index fa66f78..27108a6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO.md b/TODO.md index 87cab21..38158b5 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 219750b..2c9d6bb 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -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, } } diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index 4c252ad..d5f2322 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -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 + } +} diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index fb64f26..3ef532a 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -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) + } +} diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 5d8a2a3..84cf2e2 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -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", diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index f5336b2..87c27d1 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -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) diff --git a/source/ir/ir.hon b/source/ir/ir.hon index a409210..4b90188 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -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 { diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index 3604801..6e2b24d 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -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 diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index 093324a..0fcb013 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -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)) +} diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 439bcdf..9483b35 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -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 diff --git a/source/llvm/instructions.hon b/source/llvm/instructions.hon index e9febc7..1aaded9 100644 --- a/source/llvm/instructions.hon +++ b/source/llvm/instructions.hon @@ -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 { diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon index aa69f58..584202f 100644 --- a/source/llvm/llvm.hon +++ b/source/llvm/llvm.hon @@ -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 } } diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 54d77d9..0ae7b0f 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -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: { diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 88b69d9..1a43540 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -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 + } +} diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 11ef796..8932121 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -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) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 9342a6d..15bbd94 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -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 diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index 50563a7..81a54a1 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -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) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index ada4b76..edf3549 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -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) ) } diff --git a/tests/native.py b/tests/native.py index 24911bf..8d54187 100644 --- a/tests/native.py +++ b/tests/native.py @@ -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 }}