diff --git a/LANGUAGE.md b/LANGUAGE.md index 2e573fe..968ab1c 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -39,8 +39,8 @@ roadmap and milestone history. ### expressions and control flow -- checked integer `+ - * /`, unary `-`, divide-by-zero traps, IEEE float arithmetic, comparisons, `!`, `and`, and `or` -- assignments and compound assignments `+= -= *= /=` with single evaluation of complex lvalues +- checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or` +- assignments and compound assignments `+= -= *= /=` with single evaluation of complex lvalues; `/=` is float-only - field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support - boolean `if` / `else if` / `else`, braceless single-statement branches, and optional parenthesized conditions - `while` loops with optional post-iteration update clauses @@ -52,6 +52,50 @@ roadmap and milestone history. - fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks - direct `return match ...` and `yield match ...` value-control-flow operands +#### division + +`/` and `/=` accept only floating-point operands. Integer division must state its rounding and +remainder convention with one of these unqualified builtins: + +| Builtin | Result | +| --- | --- | +| `div_trunc(a, b)` | quotient rounded toward zero | +| `div_floor(a, b)` | quotient rounded toward negative infinity | +| `div_exact(a, b)` | truncated quotient; traps unless it divides exactly | +| `div_ceil(a, b)` | quotient rounded toward positive infinity | +| `rem(a, b)` | remainder paired with `div_trunc`; sign follows `a` | +| `mod(a, b)` | modulus paired with `div_floor`; sign follows `b` | + +The operands may be compatible concrete integer or float scalars. Existing literal coercion and +numeric widening rules apply, the result has the common operand type, and float quotients are +integral-valued floats. These identities hold when representable: + +```bro +div_trunc(a, b) * b + rem(a, b) == a +div_floor(a, b) * b + mod(a, b) == a +``` + +Negative operands distinguish the operations: + +```bro +div_trunc(-5, 3) == -1 +div_floor(-5, 3) == -2 +div_ceil(-5, 3) == -1 +rem(-5, 3) == -2 +mod(-5, 3) == 1 +mod(5, -3) == -1 +``` + +All six builtins diagnose a zero denominator at comptime and trap at runtime, including float +zero. Quotient operations also trap for signed `min_value(T), -1`; `rem` and `mod` return zero for +that pair. `div_exact` traps when `div_trunc(a, b) * b == a` is false in the operand type, so float +exactness follows floating-point equality. Other float NaN and infinity behavior follows the +underlying IEEE operations. Ordinary float `/` remains unchecked and therefore preserves IEEE +infinity/NaN behavior. + +The six spellings are reserved only as direct unqualified calls. A qualified call such as +`math.div_floor(a, b)` resolves to an ordinary package function. + ### functions, C interop, and linking - demand-monomorphized Brolang and C-ABI functions diff --git a/README.md b/README.md index 340a95e..793b7df 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ Current prototype features: - Bodyless manual and imported C variadic declarations with default argument promotions - Ordered linking of additional C sources, objects, archives, and libraries - Checked signed addition and unary negation +- Float-only `/` plus explicit `div_trunc`, `div_floor`, `div_exact`, `div_ceil`, `rem`, and `mod` scalar builtins - Static, eager runtime, mutable runtime, and deferred problematic globals - Runtime diagnostics followed by `llvm.trap` diff --git a/TODO.md b/TODO.md index bb47574..be8a802 100644 --- a/TODO.md +++ b/TODO.md @@ -116,7 +116,7 @@ - `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized - for all conditionals/guards, parentheses are optional but allowed for visual clarity -6. compound assignment: `+=`, `-=`, `*=`, `/=` (implemented) +6. compound assignment: `+=`, `-=`, `*=`, `/=` (implemented; division semantics superseded by milestone 32) - added the binary arithmetic operators `-`, `*`, `/` (previously only `+` existed); `*`/`/` bind tighter than `+`/`-`, and prefix `-` (negation) is unchanged - compound assignments preserve the target, operator, and right-hand side explicitly through @@ -124,10 +124,9 @@ operation, and stores through that address - side-effecting index, field-base, and dereference expressions are evaluated once in left-to-right order - - integer arithmetic traps on overflow (`Sub_Checked`/`Mul_Checked` via the LLVM - `.with.overflow` intrinsics) and integer `/` traps on divide-by-zero and `INT_MIN / -1`; - floats follow IEEE (`fadd`/`fsub`/`fmul`/`fdiv`, no trap) - - constant folding (global initializers) covers `-`, `*`, `/` alongside `+` + - integer `+`, `-`, and `*` trap on overflow; milestone 32 later restricted `/` and `/=` to + floats and introduced the explicit integer/float division family + - constant folding (global initializers) covers the arithmetic family 7. enums (native and c interop) (implemented; see below) - native enums are nominal value types with integer runtime representations @@ -791,10 +790,24 @@ - the existing specialization/HIR/LLVM ABI is unchanged; `std/mem` and `std/arraylist` now use the inferred form where their arguments or result provide enough information -32. disallow arbitrary integer division - - take inspiration from zig - - see also below for a word on unchecked casts - - the user should be explicit about what they mean with integer division (e.g. `div`, `rem`) +32. explicit division family (implemented) + - `/` and `/=` are float-only; every integer use is rejected with guidance toward explicit + division, including literals, comptime execution, array counts, and compound assignment + - direct unqualified calls reserve `div_trunc`, `div_floor`, `div_exact`, `div_ceil`, `rem`, and + `mod`; qualified names remain ordinary package functions + - the builtins accept compatible concrete integer or float scalars, reuse existing literal and + widening rules, and return the common operand type (integral-valued floats for quotients) + - all builtins diagnose zero denominators at comptime and trap at runtime; quotient operations + also trap on signed `min_value(T) / -1`, while `rem` and `mod` return zero for that pair + - `div_exact` checks the reconstructed dividend in the operand type; `rem` pairs with truncation + and follows the numerator sign, while `mod` pairs with floor and follows the denominator sign + - HIR/IR use compact semantic enum tags; integer floor, ceil, and exact lowering reconstructs the + remainder from one quotient so each produces only one hardware-division candidate + - float lowering uses the typed LLVM trunc/floor/ceil intrinsics, `frem`, and ordered equality; + ordinary float `/` remains the unchecked IEEE infinity/NaN escape hatch + - migrated `std/mem`, `std/arraylist`, and the compound-assignment example to `div_trunc` + +33. design io interface ## A word on unchecked casts diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 8e13a20..db53a71 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -399,6 +399,31 @@ Type_Builtin :: enum u8 { Max_Value, } +Division_Builtin :: enum u8 { + None, + Trunc, + Floor, + Exact, + Ceil, + Rem, + Mod, +} + +division_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Division_Builtin { + if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) { + return .None + } + switch symbol_text(checker, expr.name) { + case "div_trunc": return .Trunc + case "div_floor": return .Floor + case "div_exact": return .Exact + case "div_ceil": return .Ceil + case "rem": return .Rem + case "mod": return .Mod + } + return .None +} + type_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Type_Builtin { if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) { return .None @@ -598,6 +623,10 @@ type_from_syntax :: proc( changed = true } } else { + if constant.kind == .Integer_Division { + source.add(checker.diagnostics, span, "integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil") + return types.INVALID + } source.add(checker.diagnostics, span, "array count must be a compile-time integer expression") return types.INVALID } @@ -2568,6 +2597,35 @@ infer_nested_expr :: proc( return result } +infer_division_builtin :: proc( + checker: ^Checker, + expr: ast.Expr, + locals: []Infer_Local, + pkg: ast.Package_Id, + file: ast.File_Id, + demanded: ^[dynamic]Spec_Id, + local_types: []types.Type, + expected: types.Type, +) -> types.Type { + if len(expr.args) != 2 { + return types.INVALID + } + hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID + left_const := is_numeric_constant_expr(checker, expr.args[0]) + right_const := is_numeric_constant_expr(checker, expr.args[1]) + left, right := types.INVALID, types.INVALID + if left_const && !right_const && !types.is_valid(hint) { + right = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types) + left = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, right) + } else { + left = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, hint) + right_hint := hint if types.is_valid(hint) else left + right = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types, right_hint) + } + result := types.widest(left, right) + return result if types.is_concrete_scalar(result) && !types.is_bool(result) else types.INVALID +} + infer_compound_expr :: proc( checker: ^Checker, expr: ast.Expr, @@ -2938,6 +2996,11 @@ infer_expr :: proc( _ = pop(&stack) continue } + if division_builtin_call(checker, expr) != .None { + last = infer_division_builtin(checker, expr, locals, pkg, file, demanded, local_types, frame.expected) + _ = pop(&stack) + continue + } if is_ptr_cast_call(checker, expr) { if len(expr.args) != 2 { last = types.INVALID @@ -3952,6 +4015,12 @@ record_demand :: proc( right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file) return left || right } + case .Call: + if division_builtin_call(checker, expr) != .None && len(expr.args) == 2 && is_numeric_demand(demand, checker.target) { + left := record_demand(checker, expr.args[0], demand, locals, local_types, pkg, file) + right := record_demand(checker, expr.args[1], demand, locals, local_types, pkg, file) + return left || right + } } return false } @@ -4435,6 +4504,14 @@ build_constant_expr :: proc( id := source.add(checker.diagnostics, expr.span, "division by zero in constant expression") return invalid_hir_expr(checker, expr.span, id, recovery_type) } + if constant.kind == .Non_Exact { + id := source.add(checker.diagnostics, expr.span, "exact division has a remainder") + return invalid_hir_expr(checker, expr.span, id, recovery_type) + } + if constant.kind == .Integer_Division { + id := source.add(checker.diagnostics, expr.span, "integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil") + return invalid_hir_expr(checker, expr.span, id, recovery_type) + } if constant.kind == .Overflow || (!types.is_concrete_integer(expected) && !fits_i64(constant.value)) { id := source.add( @@ -4868,6 +4945,89 @@ build_nested_expr :: proc( return result } +try_build_comptime_division :: proc( + checker: ^Checker, + expr: ast.Expr, + kind: Division_Builtin, + expected: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> (hir.Expr_Id, bool) { + state := ct_state_make(checker, pkg, file, diagnose=false) + defer ct_state_destroy(&state) + value, flow, ok := ct_eval_division_call(&state, expr, kind, expected, 0) + if ok && flow.kind == .Normal && value != INVALID_CT_VALUE { + return ct_materialize_value(&state, value, expr.span, expected), true + } + message := "" + #partial switch state.error { + case .Div_By_Zero: message = "division builtin denominator is zero" + case .Overflow: message = "signed integer division overflow" + case .Non_Exact: message = "exact division has a remainder" + } + if len(message) == 0 { + return hir.INVALID_EXPR, false + } + id := source.add(checker.diagnostics, expr.span, message) + recovery := expected if types.is_concrete_scalar(expected) else types.I64 + return invalid_hir_expr(checker, expr.span, id, recovery), true +} + +build_division_builtin :: proc( + checker: ^Checker, + expr: ast.Expr, + kind: Division_Builtin, + locals: []Build_Local, + global_reads: ^[dynamic]hir.Global_Id, + calls: ^[dynamic]hir.Function_Id, + expected: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> hir.Expr_Id { + if len(expr.args) != 2 { + id := source.addf( + checker.diagnostics, expr.span, "%s expects 2 arguments, got %d", + symbol_text(checker, expr.name), len(expr.args), + ) + return invalid_hir_expr(checker, expr.span, id) + } + if value, handled := try_build_comptime_division(checker, expr, kind, expected, pkg, file); handled { + return value + } + hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID + left_const := is_numeric_constant_expr(checker, expr.args[0]) + right_const := is_numeric_constant_expr(checker, expr.args[1]) + left, right := hir.INVALID_EXPR, hir.INVALID_EXPR + if left_const && !right_const && !types.is_valid(hint) { + right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, types.INVALID, pkg, file) + left = build_nested_expr(checker, expr.args[0], locals, global_reads, calls, checker.module.exprs[right].type, pkg, file) + } else { + left = build_nested_expr(checker, expr.args[0], locals, global_reads, calls, hint, pkg, file) + right_hint := hint if types.is_valid(hint) else checker.module.exprs[left].type + right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, right_hint, pkg, file) + } + result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) + if !types.is_concrete_scalar(result) || types.is_bool(result) { + id := source.add(checker.diagnostics, expr.span, "division builtins require compatible numeric operands") + return invalid_hir_expr(checker, expr.span, id) + } + left = coerce_expr(checker, left, result, checker.module.exprs[left].span) + right = coerce_expr(checker, right, result, checker.module.exprs[right].span) + result_kind := hir.Expr_Kind.Div_Trunc + #partial switch kind { + case .Floor: result_kind = .Div_Floor + case .Exact: result_kind = .Div_Exact + case .Ceil: result_kind = .Div_Ceil + case .Rem: result_kind = .Rem + case .Mod: result_kind = .Mod + case: + } + return add_hir_expr(checker, hir.Expr{ + kind=result_kind, span=expr.span, type=result, left=left, right=right, + target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + fallible_aggregate :: proc( checker: ^Checker, span: source.Span, @@ -5578,6 +5738,13 @@ build_binary_arith :: proc( id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands") return invalid_hir_expr(checker, span, id) } + if op == .Div && !types.is_float(result, checker.target) { + id := source.add( + checker.diagnostics, span, + "integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil", + ) + return invalid_hir_expr(checker, span, id, result) + } result_kind := hir.Expr_Kind.Add #partial switch op { case .Sub: result_kind = .Sub @@ -5627,7 +5794,7 @@ build_expr :: proc( expr := checker.ast_module.exprs[frame.expr] if frame.stage == 0 { constant := eval_constant(checker, frame.expr) - if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero { + if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero || constant.kind == .Non_Exact { last = build_constant_expr(checker, expr, constant, frame.expected) _ = pop(&stack) continue @@ -5807,6 +5974,13 @@ build_expr :: proc( _ = pop(&stack) continue } + if builtin := division_builtin_call(checker, expr); builtin != .None { + last = build_division_builtin( + checker, expr, builtin, locals, global_reads, calls, frame.expected, pkg, file, + ) + _ = pop(&stack) + continue + } if is_ptr_cast_call(checker, expr) { if len(expr.args) != 2 { id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args)) @@ -6653,7 +6827,14 @@ build_block :: proc( } rhs_type := checker.module.exprs[value].type result_type := types.widest(target_type, rhs_type) - if !types.is_concrete_scalar(result_type) || + if statement.assignment_op == .Div && types.is_concrete_integer(result_type) { + id := source.add( + checker.diagnostics, + statement.span, + "integer '/=' is not allowed; assign through an explicit division builtin", + ) + value = invalid_hir_expr(checker, statement.span, id, target_type) + } else if !types.is_concrete_scalar(result_type) || types.is_bool(result_type) { id := source.add( checker.diagnostics, diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 3b29fce..8af6735 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -6,6 +6,8 @@ import "../source" import "../symbol" import "../types" import "base:intrinsics" + +import "core:math" import "core:mem" COMPTIME_EVAL_QUOTA :: 100_000 @@ -28,6 +30,8 @@ Constant_Kind :: enum { Value, Overflow, Div_By_Zero, + Non_Exact, + Integer_Division, } Constant :: struct { @@ -94,8 +98,7 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { continue } expr := checker.ast_module.exprs[frame.expr] - if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && - expr.kind != .Div && expr.kind != .Negate { + if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && expr.kind != .Negate { result := Constant{kind = .Not_Constant} if expr.kind == .Integer { result = Constant{kind = .Value, value = i128(expr.integer)} @@ -118,9 +121,7 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { operand = checker.constants[expr.left] } result := Constant{kind = .Not_Constant} - if operand.kind == .Div_By_Zero { - result = Constant{kind = .Div_By_Zero} - } else if operand.kind == .Overflow { + if operand.kind == .Overflow { result = Constant{kind = .Overflow} } else if operand.kind == .Value { value, overflow := intrinsics.overflow_sub(i128(0), operand.value) @@ -147,29 +148,19 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { right = checker.constants[expr.right] } result := Constant{kind = .Not_Constant} - if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero { - result = Constant{kind = .Div_By_Zero} - } else if left.kind == .Overflow || right.kind == .Overflow { + if left.kind == .Overflow || right.kind == .Overflow { result = Constant{kind = .Overflow} } else if left.kind == .Value && right.kind == .Value { value: i128 overflow: bool - div_by_zero: bool #partial switch expr.kind { case .Sub: value, overflow = intrinsics.overflow_sub(left.value, right.value) case .Mul: value, overflow = intrinsics.overflow_mul(left.value, right.value) - case .Div: - if right.value == 0 { - div_by_zero = true - } else { - value = left.value / right.value - } case: value, overflow = intrinsics.overflow_add(left.value, right.value) } switch { - case div_by_zero: result = Constant{kind = .Div_By_Zero} - case overflow: result = Constant{kind = .Overflow} - case: result = Constant{kind = .Value, value = value} + case overflow: result = Constant{kind = .Overflow} + case: result = Constant{kind = .Value, value = value} } } checker.constants[frame.expr] = result @@ -226,6 +217,8 @@ Ct_Error_Kind :: enum u8 { Not_Comptime, Overflow, Div_By_Zero, + Non_Exact, + Integer_Division, Quota, } @@ -1105,7 +1098,8 @@ ct_eval_expr :: proc( } return ct_eval_unary(state, expr.kind, value, expr.span) case .Add, .Sub, .Mul, .Div, .Eq, .Ne, .Lt, .Le, .Gt, .Ge: - left, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + left_expected := expected if expr.kind == .Div && types.is_float(expected, checker.target) else types.INVALID + left, flow, ok := ct_eval_expr(state, expr.left, left_expected, depth+1) if !ok || flow.kind != .Normal { return INVALID_CT_VALUE, flow, ok } @@ -1858,6 +1852,12 @@ ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, left_id, right_id: C } return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if ok else 0}), ct_flow(.Normal), true } + if op == .Div { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( + state, .Integer_Division, span, + "integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil", + ) + } value: i128 overflow := false #partial switch op { @@ -1865,12 +1865,6 @@ ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, left_id, right_id: C value, overflow = intrinsics.overflow_sub(left.integer, right.integer) case .Mul: value, overflow = intrinsics.overflow_mul(left.integer, right.integer) - case .Div: - if right.integer == 0 { - state.error = .Div_By_Zero - return INVALID_CT_VALUE, ct_flow(.Normal), false - } - value = left.integer / right.integer case: value, overflow = intrinsics.overflow_add(left.integer, right.integer) } @@ -1887,6 +1881,137 @@ ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, left_id, right_id: C return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime binary expression requires compatible operands") } +ct_eval_division_builtin :: proc( + state: ^Ct_State, + kind: Division_Builtin, + left_id, right_id: Ct_Value_Id, + span: source.Span, +) -> (Ct_Value_Id, Ct_Flow, bool) { + if left_id == INVALID_CT_VALUE || right_id == INVALID_CT_VALUE || + int(left_id) >= len(state.values) || int(right_id) >= len(state.values) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + left := state.values[left_id] + right := state.values[right_id] + result_type := types.widest(left.type, right.type) + if !types.is_concrete_scalar(result_type) || types.is_bool(result_type) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( + state, .Not_Comptime, span, "division builtins require compatible numeric operands", + ) + } + left_id, left_ok := ct_coerce_value(state, left_id, result_type, span) + right_id, right_ok := ct_coerce_value(state, right_id, result_type, span) + if !left_ok || !right_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + left = state.values[left_id] + right = state.values[right_id] + if left.kind == .Float && right.kind == .Float { + if right.float == 0 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Div_By_Zero, span, "division builtin denominator is zero") + } + quotient := left.float / right.float + result := quotient + #partial switch kind { + case .Trunc: result = math.trunc(quotient) + case .Floor: result = math.floor(quotient) + case .Ceil: result = math.ceil(quotient) + case .Exact: + result = math.trunc(quotient) + if result * right.float != left.float { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Non_Exact, span, "exact division has a remainder") + } + case .Rem, .Mod: + result = left.float - math.trunc(quotient) * right.float + if kind == .Mod && result != 0 && (result < 0) != (right.float < 0) { + result += right.float + } + } + if types.bits(result_type, state.checker.target) == 32 { + result = f64(f32(result)) + } + return ct_add_value(state, Ct_Value{kind=.Float, type=result_type, float=result}), ct_flow(.Normal), true + } + if left.kind != .Integer || right.kind != .Integer { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "division builtins require compatible numeric operands") + } + if right.integer == 0 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Div_By_Zero, span, "division builtin denominator is zero") + } + is_quotient := kind == .Trunc || kind == .Floor || kind == .Exact || kind == .Ceil + if is_quotient && types.is_signed(result_type, state.checker.target) { + minimum := -(i128(1) << u32(types.bits(result_type, state.checker.target)-1)) + if left.integer == minimum && right.integer == -1 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Overflow, span, "signed integer division overflow") + } + } + quotient := left.integer / right.integer + remainder := left.integer % right.integer + result := quotient + #partial switch kind { + case .Floor: + if remainder != 0 && (left.integer < 0) != (right.integer < 0) { + result -= 1 + } + case .Ceil: + if remainder != 0 && (left.integer < 0) == (right.integer < 0) { + result += 1 + } + case .Exact: + if remainder != 0 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Non_Exact, span, "exact division has a remainder") + } + case .Rem: result = remainder + case .Mod: + result = remainder + if result != 0 && (result < 0) != (right.integer < 0) { + result += right.integer + } + case: + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=result_type, integer=result}), ct_flow(.Normal), true +} + +ct_eval_division_call :: proc( + state: ^Ct_State, + expr: ast.Expr, + kind: Division_Builtin, + expected: types.Type, + depth: int, +) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + if len(expr.args) != 2 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( + state, .Not_Comptime, expr.span, "%s expects 2 arguments, got %d", + symbol_text(checker, expr.name), len(expr.args), + ) + } + hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID + left_const := is_numeric_constant_expr(checker, expr.args[0]) + right_const := is_numeric_constant_expr(checker, expr.args[1]) + left, right := INVALID_CT_VALUE, INVALID_CT_VALUE + flow := ct_flow(.Normal) + ok := false + if left_const && !right_const && !types.is_valid(hint) { + right, flow, ok = ct_eval_expr(state, expr.args[1], types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + left, flow, ok = ct_eval_expr(state, expr.args[0], state.values[right].type, depth+1) + } else { + left, flow, ok = ct_eval_expr(state, expr.args[0], hint, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + right_hint := hint if types.is_valid(hint) else state.values[left].type + right, flow, ok = ct_eval_expr(state, expr.args[1], right_hint, depth+1) + } + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + return ct_eval_division_builtin(state, kind, left, right, expr.span) +} + ct_scalar_cast :: proc(state: ^Ct_State, id: Ct_Value_Id, target: types.Type, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { if id == INVALID_CT_VALUE || int(id) >= len(state.values) { return INVALID_CT_VALUE, ct_flow(.Normal), false @@ -1943,6 +2068,9 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type result_type := types.USIZE if builtin == .Size_Of || builtin == .Align_Of else target return ct_add_value(state, Ct_Value{kind=.Integer, type=result_type, integer=type_builtin_value(checker, builtin, target)}), ct_flow(.Normal), true } + if builtin := division_builtin_call(checker, expr); builtin != .None { + return ct_eval_division_call(state, expr, builtin, expected, depth+1) + } target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false) if !available { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package") @@ -2899,6 +3027,10 @@ eval_integer_constant_in_context :: proc( return Constant{kind=.Overflow} case .Div_By_Zero: return Constant{kind=.Div_By_Zero} + case .Non_Exact: + return Constant{kind=.Non_Exact} + case .Integer_Division: + return Constant{kind=.Integer_Division} } return Constant{kind=.Not_Constant} } @@ -2927,6 +3059,10 @@ eval_comptime_statements :: proc( return Constant{kind=.Overflow}, false, false case .Div_By_Zero: return Constant{kind=.Div_By_Zero}, false, false + case .Non_Exact: + return Constant{kind=.Non_Exact}, false, false + case .Integer_Division: + return Constant{kind=.Integer_Division}, false, false } return Constant{kind=.Not_Constant}, false, false } @@ -2958,6 +3094,10 @@ eval_comptime_call :: proc( return Constant{kind=.Overflow} case .Div_By_Zero: return Constant{kind=.Div_By_Zero} + case .Non_Exact: + return Constant{kind=.Non_Exact} + case .Integer_Division: + return Constant{kind=.Integer_Division} } return Constant{kind=.Not_Constant} } @@ -2996,7 +3136,7 @@ infer_comptime_expr_type :: proc( } } if !ok || flow.kind != .Normal || value == INVALID_CT_VALUE || int(value) >= len(state.values) { - if state.error == .Overflow || state.error == .Div_By_Zero { + if state.error == .Overflow || state.error == .Div_By_Zero || state.error == .Non_Exact || state.error == .Integer_Division { return types.I64 } return types.INVALID @@ -3038,6 +3178,12 @@ build_comptime_expr :: proc( if state.error == .Overflow { return build_constant_expr(checker, expr, Constant{kind=.Overflow}, expected) } + if state.error == .Non_Exact { + return build_constant_expr(checker, expr, Constant{kind=.Non_Exact}, expected) + } + if state.error == .Integer_Division { + return build_constant_expr(checker, expr, Constant{kind=.Integer_Division}, expected) + } diagnostic := state.diagnostic if diagnostic == source.INVALID_DIAGNOSTIC { diagnostic = source.add(checker.diagnostics, expr.span, "expression cannot be evaluated at comptime") diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index b99e0a9..6515449 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -113,6 +113,12 @@ Expr_Kind :: enum u8 { Sub, Mul, Div, + Div_Trunc, + Div_Floor, + Div_Exact, + Div_Ceil, + Rem, + Mod, Pointer_Add, Eq, Ne, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index b3cf6f3..6ac7ef4 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -109,6 +109,12 @@ Opcode :: enum u8 { Sub_Checked, Mul_Checked, Div_Checked, + Div_Trunc_Checked, + Div_Floor_Checked, + Div_Exact_Checked, + Div_Ceil_Checked, + Rem_Checked, + Mod_Checked, Pointer_Add, Not, Compare, diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 6edbcfd..c97a487 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -257,7 +257,10 @@ valid_value :: proc( .Fallible_Error, .Extract, .Select, .Unwrap, .Optional_Is_Some, .Optional_Value, .Orelse, .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Pointer_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, - .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: + .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, + .Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked, + .Rem_Checked, .Mod_Checked, + .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, .Store, .Fill, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: @@ -582,9 +585,7 @@ emit_checked_arithmetic :: proc( fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_index) } -// emit_checked_division emits a trapping integer division guarding divide-by-zero -// and signed `INT_MIN / -1` overflow, or a plain floating-point division. -emit_checked_division :: proc( +emit_division_zero_guard :: proc( emitter: ^Emitter, instructions: []ir.Instruction, instruction_index: int, @@ -592,56 +593,225 @@ emit_checked_division :: proc( ) { type_name := llvm_type(instruction.type, &emitter.module.types) if types.is_float(instruction.type, emitter.module.target) { - fmt.sbprintf(&emitter.builder, " %%v%d = fdiv %s ", instruction_index, type_name) - write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) - strings.write_string(&emitter.builder, ", ") + fmt.sbprintf(&emitter.builder, " %%divzero%d = fcmp oeq %s ", instruction_index, type_name) write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) - strings.write_string(&emitter.builder, "\n") - return + strings.write_string(&emitter.builder, ", 0.000000e+00\n") + } else { + fmt.sbprintf(&emitter.builder, " %%divzero%d = icmp eq %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", 0\n") } - signed := !types.is_unsigned(instruction.type, emitter.module.target) - fmt.sbprintf(&emitter.builder, " %%divzero%d = icmp eq %s ", instruction_index, type_name) - write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) - strings.write_string(&emitter.builder, ", 0\n") fmt.sbprintf( &emitter.builder, - " br i1 %%divzero%d, label %%divzero_trap%d, label %%divzero_ok%d\n", + " br i1 %%divzero%d, label %%divzero_trap%d, label %%divzero_ok%d\ndivzero_trap%d:\n", + instruction_index, instruction_index, instruction_index, instruction_index, ) - fmt.sbprintf(&emitter.builder, "divzero_trap%d:\n", instruction_index) - zero_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "integer division by zero") - emit_trap_call(emitter, zero_message) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "division builtin denominator is zero") + emit_trap_call(emitter, message) fmt.sbprintf(&emitter.builder, " unreachable\ndivzero_ok%d:\n", instruction_index) - if signed { - min_value := -(i128(1) << u32(types.bits(instruction.type, emitter.module.target) - 1)) - fmt.sbprintf(&emitter.builder, " %%divminlo%d = icmp eq %s ", instruction_index, type_name) +} + +emit_division_overflow_guard :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, +) { + type_name := llvm_type(instruction.type, &emitter.module.types) + min_value := -(i128(1) << u32(types.bits(instruction.type, emitter.module.target)-1)) + fmt.sbprintf(&emitter.builder, " %%divminlo%d = icmp eq %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %d\n %%divminhi%d = icmp eq %s ", min_value, instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf( + &emitter.builder, + ", -1\n %%divovf%d = and i1 %%divminlo%d, %%divminhi%d\n br i1 %%divovf%d, label %%divovf_trap%d, label %%divovf_ok%d\ndivovf_trap%d:\n", + instruction_index, + instruction_index, + instruction_index, + instruction_index, + instruction_index, + instruction_index, + instruction_index, + ) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "signed integer division overflow") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\ndivovf_ok%d:\n", instruction_index) +} + +emit_float_division_builtin :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, +) { + type_name := llvm_type(instruction.type, &emitter.module.types) + suffix := "f32" if types.bits(instruction.type, emitter.module.target) == 32 else "f64" + + if instruction.op == .Rem_Checked || instruction.op == .Mod_Checked { + name := fmt.tprintf("%%v%d", instruction_index) if instruction.op == .Rem_Checked else fmt.tprintf("%%rawrem%d", instruction_index) + fmt.sbprintf(&emitter.builder, " %s = frem %s ", name, type_name) write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) - fmt.sbprintf(&emitter.builder, ", %d\n", min_value) - fmt.sbprintf(&emitter.builder, " %%divminhi%d = icmp eq %s ", instruction_index, type_name) + strings.write_string(&emitter.builder, ", ") write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) - strings.write_string(&emitter.builder, ", -1\n") - fmt.sbprintf(&emitter.builder, " %%divovf%d = and i1 %%divminlo%d, %%divminhi%d\n", instruction_index, instruction_index, instruction_index) - fmt.sbprintf( - &emitter.builder, - " br i1 %%divovf%d, label %%divovf_trap%d, label %%divovf_ok%d\n", - instruction_index, - instruction_index, - instruction_index, - ) - fmt.sbprintf(&emitter.builder, "divovf_trap%d:\n", instruction_index) - ovf_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "signed integer division overflow") - emit_trap_call(emitter, ovf_message) - fmt.sbprintf(&emitter.builder, " unreachable\ndivovf_ok%d:\n", instruction_index) - fmt.sbprintf(&emitter.builder, " %%v%d = sdiv %s ", instruction_index, type_name) - } else { - fmt.sbprintf(&emitter.builder, " %%v%d = udiv %s ", instruction_index, type_name) + strings.write_string(&emitter.builder, "\n") + if instruction.op == .Rem_Checked { + return + } + fmt.sbprintf(&emitter.builder, " %%remnonzero%d = fcmp one %s %%rawrem%d, 0.000000e+00\n", instruction_index, type_name, instruction_index) + fmt.sbprintf(&emitter.builder, " %%remsign%d = fcmp olt %s %%rawrem%d, 0.000000e+00\n", instruction_index, type_name, instruction_index) + fmt.sbprintf(&emitter.builder, " %%denomsign%d = fcmp olt %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", 0.000000e+00\n") + fmt.sbprintf(&emitter.builder, " %%signsdiffer%d = xor i1 %%remsign%d, %%denomsign%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%modadjust%d = and i1 %%remnonzero%d, %%signsdiffer%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%adjustedrem%d = fadd %s %%rawrem%d, ", instruction_index, type_name, instruction_index) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n %%v%d = select i1 %%modadjust%d, %s %%adjustedrem%d, %s %%rawrem%d\n", instruction_index, instruction_index, type_name, instruction_index, type_name, instruction_index) + return } + + fmt.sbprintf(&emitter.builder, " %%divq%d = fdiv %s ", instruction_index, type_name) write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) strings.write_string(&emitter.builder, ", ") write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) strings.write_string(&emitter.builder, "\n") + intrinsic := "trunc" + if instruction.op == .Div_Floor_Checked { + intrinsic = "floor" + } else if instruction.op == .Div_Ceil_Checked { + intrinsic = "ceil" + } + fmt.sbprintf(&emitter.builder, " %%v%d = call %s @llvm.%s.%s(%s %%divq%d)\n", instruction_index, type_name, intrinsic, suffix, type_name, instruction_index) + if instruction.op != .Div_Exact_Checked { + return + } + fmt.sbprintf(&emitter.builder, " %%exactprod%d = fmul %s %%v%d, ", instruction_index, type_name, instruction_index) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n %%exact%d = fcmp oeq %s %%exactprod%d, ", instruction_index, type_name, instruction_index) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n br i1 %%exact%d, label %%exact_ok%d, label %%exact_trap%d\nexact_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "exact division has a remainder") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\nexact_ok%d:\n", instruction_index) +} + +emit_integer_remainder_builtin :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, +) { + type_name := llvm_type(instruction.type, &emitter.module.types) + signed := types.is_signed(instruction.type, emitter.module.target) + raw_name := fmt.tprintf("%%v%d", instruction_index) if instruction.op == .Rem_Checked || !signed else fmt.tprintf("%%rawrem%d", instruction_index) + if !signed { + fmt.sbprintf(&emitter.builder, " %s = urem %s ", raw_name, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + } else { + min_value := -(i128(1) << u32(types.bits(instruction.type, emitter.module.target)-1)) + fmt.sbprintf(&emitter.builder, " %%remminlo%d = icmp eq %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %d\n %%remminhi%d = icmp eq %s ", min_value, instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", -1\n %%remspecial%d = and i1 %%remminlo%d, %%remminhi%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " br i1 %%remspecial%d, label %%rem_special%d, label %%rem_normal%d\nrem_special%d:\n br label %%rem_join%d\nrem_normal%d:\n", instruction_index, instruction_index, instruction_index, instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%remnormal%d = srem %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n br label %%rem_join%d\nrem_join%d:\n %s = phi %s [ 0, %%rem_special%d ], [ %%remnormal%d, %%rem_normal%d ]\n", instruction_index, instruction_index, raw_name, type_name, instruction_index, instruction_index, instruction_index) + } + if instruction.op == .Rem_Checked || !signed { + return + } + fmt.sbprintf(&emitter.builder, " %%remnonzero%d = icmp ne %s %%rawrem%d, 0\n", instruction_index, type_name, instruction_index) + fmt.sbprintf(&emitter.builder, " %%remsign%d = icmp slt %s %%rawrem%d, 0\n", instruction_index, type_name, instruction_index) + fmt.sbprintf(&emitter.builder, " %%denomsign%d = icmp slt %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", 0\n %%signsdiffer%d = xor i1 %%remsign%d, %%denomsign%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%modadjust%d = and i1 %%remnonzero%d, %%signsdiffer%d\n %%adjustedrem%d = add %s %%rawrem%d, ", instruction_index, instruction_index, instruction_index, instruction_index, type_name, instruction_index) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n %%v%d = select i1 %%modadjust%d, %s %%adjustedrem%d, %s %%rawrem%d\n", instruction_index, instruction_index, type_name, instruction_index, type_name, instruction_index) +} + +emit_integer_quotient_builtin :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, +) { + type_name := llvm_type(instruction.type, &emitter.module.types) + signed := types.is_signed(instruction.type, emitter.module.target) + operation := "sdiv" if signed else "udiv" + name := fmt.tprintf("%%v%d", instruction_index) if instruction.op == .Div_Trunc_Checked else fmt.tprintf("%%divq%d", instruction_index) + fmt.sbprintf(&emitter.builder, " %s = %s %s ", name, operation, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + if instruction.op == .Div_Trunc_Checked { + return + } + fmt.sbprintf(&emitter.builder, " %%divprod%d = mul %s %%divq%d, ", instruction_index, type_name, instruction_index) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n %%divrem%d = sub %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %%divprod%d\n", instruction_index) + if instruction.op == .Div_Exact_Checked { + fmt.sbprintf(&emitter.builder, " %%exact%d = icmp eq %s %%divrem%d, 0\n br i1 %%exact%d, label %%exact_ok%d, label %%exact_trap%d\nexact_trap%d:\n", instruction_index, type_name, instruction_index, instruction_index, instruction_index, instruction_index, instruction_index) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "exact division has a remainder") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\nexact_ok%d:\n %%v%d = add %s %%divq%d, 0\n", instruction_index, instruction_index, type_name, instruction_index) + return + } + fmt.sbprintf(&emitter.builder, " %%remnonzero%d = icmp ne %s %%divrem%d, 0\n", instruction_index, type_name, instruction_index) + if signed { + fmt.sbprintf(&emitter.builder, " %%numsign%d = icmp slt %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", 0\n %%denomsign%d = icmp slt %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", 0\n %%signsdiffer%d = xor i1 %%numsign%d, %%denomsign%d\n", instruction_index, instruction_index, instruction_index) + predicate := fmt.tprintf("%%signsdiffer%d", instruction_index) + if instruction.op == .Div_Ceil_Checked { + fmt.sbprintf(&emitter.builder, " %%signssame%d = xor i1 %%signsdiffer%d, true\n", instruction_index, instruction_index) + predicate = fmt.tprintf("%%signssame%d", instruction_index) + } + fmt.sbprintf(&emitter.builder, " %%divadjust%d = and i1 %%remnonzero%d, %s\n", instruction_index, instruction_index, predicate) + } else { + fmt.sbprintf(&emitter.builder, " %%divadjust%d = and i1 %%remnonzero%d, true\n", instruction_index, instruction_index) + } + adjustment := "sub" if instruction.op == .Div_Floor_Checked else "add" + fmt.sbprintf(&emitter.builder, " %%adjustedq%d = %s %s %%divq%d, 1\n %%v%d = select i1 %%divadjust%d, %s %%adjustedq%d, %s %%divq%d\n", instruction_index, adjustment, type_name, instruction_index, instruction_index, instruction_index, type_name, instruction_index, type_name, instruction_index) +} + +emit_division_builtin :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, +) { + emit_division_zero_guard(emitter, instructions, instruction_index, instruction) + if types.is_float(instruction.type, emitter.module.target) { + emit_float_division_builtin(emitter, instructions, instruction_index, instruction) + return + } + quotient := instruction.op == .Div_Trunc_Checked || instruction.op == .Div_Floor_Checked || + instruction.op == .Div_Exact_Checked || instruction.op == .Div_Ceil_Checked + if quotient && types.is_signed(instruction.type, emitter.module.target) { + emit_division_overflow_guard(emitter, instructions, instruction_index, instruction) + } + if quotient { + emit_integer_quotient_builtin(emitter, instructions, instruction_index, instruction) + } else { + emit_integer_remainder_builtin(emitter, instructions, instruction_index, instruction) + } } emit_instruction_stream :: proc( @@ -1609,11 +1779,22 @@ emit_instruction_stream :: proc( emit_checked_arithmetic(emitter, instructions, instruction_index, instruction, "mul", "fmul", "integer multiplication overflow") case .Div_Checked: if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) || - !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) { + !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) || + !types.is_float(instruction.type, emitter.module.target) { emit_recovery_value(emitter, instruction_index, instruction, "invalid division operand") continue } - emit_checked_division(emitter, instructions, instruction_index, instruction) + emit_checked_arithmetic(emitter, instructions, instruction_index, instruction, "div", "fdiv", "") + case .Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked, + .Rem_Checked, .Mod_Checked: + if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) || + !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) || + (!types.is_concrete_integer(instruction.type) && + !types.is_float(instruction.type, emitter.module.target)) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid division builtin operands") + continue + } + emit_division_builtin(emitter, instructions, instruction_index, instruction) case .Pointer_Add: result_item, result_ok := types.node(&emitter.module.types, instruction.type) base_type := instructions[instruction.a].type if valid_instruction(instructions, instruction.a) else types.INVALID @@ -2283,6 +2464,7 @@ emit_messages :: proc(emitter: ^Emitter) { emit_declarations :: proc(emitter: ^Emitter) { strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\ndeclare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)\ndeclare void @llvm.memset.p0.i64(ptr, i8, i64, i1 immarg)\n") + strings.write_string(&emitter.builder, "declare float @llvm.trunc.f32(float)\ndeclare double @llvm.trunc.f64(double)\ndeclare float @llvm.floor.f32(float)\ndeclare double @llvm.floor.f64(double)\ndeclare float @llvm.ceil.f32(float)\ndeclare double @llvm.ceil.f64(double)\n") widths := [?]int{8, 16, 32, 64} overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"} for bits in widths { diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 4fa15ce..fc7d965 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -681,7 +681,8 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Negate: stack[frame_index].stage = 5 append(&stack, Lower_Expr_Frame{expr=expr.left}) - case .Add, .Sub, .Mul, .Div, .Pointer_Add: + case .Add, .Sub, .Mul, .Div, .Div_Trunc, .Div_Floor, .Div_Exact, .Div_Ceil, + .Rem, .Mod, .Pointer_Add: stack[frame_index].stage = 2 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Call: @@ -760,6 +761,12 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Sub: op = .Sub_Checked case .Mul: op = .Mul_Checked case .Div: op = .Div_Checked + case .Div_Trunc: op = .Div_Trunc_Checked + case .Div_Floor: op = .Div_Floor_Checked + case .Div_Exact: op = .Div_Exact_Checked + case .Div_Ceil: op = .Div_Ceil_Checked + case .Rem: op = .Rem_Checked + case .Mod: op = .Mod_Checked case .Pointer_Add: op = .Pointer_Add } last = append_instruction(state, ir.Instruction{ diff --git a/compiler_tests.odin b/compiler_tests.odin index 5e687f0..7e1490b 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -6246,7 +6246,7 @@ main func() void {} @(test) constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) { - text := `value :: 5 / 0 + text := `value :: div_trunc(5, 0) main func() void {} ` source_file := source.Source{path="test.bro", text=text} @@ -6265,7 +6265,7 @@ main func() void {} found_overflow := false for diagnostic in diagnostics.items { found_division_by_zero = found_division_by_zero || - strings.contains(diagnostic.message, "division by zero in constant expression") + strings.contains(diagnostic.message, "division builtin denominator is zero") found_overflow = found_overflow || strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range") } @@ -6404,11 +6404,19 @@ malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) { malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) { module := ir.init_module() defer ir.destroy_module(&module) - instructions := make([]ir.Instruction, 4) + instructions := make([]ir.Instruction, 11) instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} - instructions[3] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} + division_ops := [?]ir.Opcode{ + .Div_Checked, + .Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked, + .Rem_Checked, .Mod_Checked, + } + for op, index in division_ops { + instructions[3+index] = ir.Instruction{op=op, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} + } + instructions[10] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} append(&module.functions, ir.Function{ link_name=strings.clone("main"), calling_convention=.C, @@ -6430,6 +6438,9 @@ malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) { testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86")) testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766")) testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846")) + for index in 3..=9 { + testing.expect(t, strings.contains(text, fmt.tprintf("%%v%d = add i32 0, -1431655766", index))) + } testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn")) testing.expect(t, !strings.contains(text, "%v-1")) llvm_path := "/tmp/brolang-test-malformed-recovery.ll" @@ -9200,12 +9211,12 @@ compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) signed += 6 signed -= 2 signed *= 3 - signed /= 4 + signed = div_trunc(signed, 4) unsigned u32 = 24 unsigned += 6 unsigned -= 2 unsigned *= 3 - unsigned /= 4 + unsigned = div_trunc(unsigned, 4) real f64 = 24.0 real += 6.0 real -= 2.0 @@ -9239,25 +9250,28 @@ compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) testing.expect_value(t, operation_counts[.Add], 3) testing.expect_value(t, operation_counts[.Sub], 3) testing.expect_value(t, operation_counts[.Mul], 3) - testing.expect_value(t, operation_counts[.Div], 3) + testing.expect_value(t, operation_counts[.Div], 1) add_count := 0 sub_count := 0 mul_count := 0 div_count := 0 + div_trunc_count := 0 for instruction in ir_module.functions[0].instructions { #partial switch instruction.op { case .Add_Checked: add_count += 1 case .Sub_Checked: sub_count += 1 case .Mul_Checked: mul_count += 1 case .Div_Checked: div_count += 1 + case .Div_Trunc_Checked: div_trunc_count += 1 case: } } testing.expect_value(t, add_count, 3) testing.expect_value(t, sub_count, 3) testing.expect_value(t, mul_count, 3) - testing.expect_value(t, div_count, 3) + testing.expect_value(t, div_count, 1) + testing.expect_value(t, div_trunc_count, 2) } @(test) @@ -9310,7 +9324,7 @@ binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) { text := `main func() i32 { a i32 = 1 b u32 = 2 - _ = a / b + _ = a + b return 0 } ` @@ -9366,7 +9380,7 @@ checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) { a i32 = 10 b i32 = 3 c i32 = a - b - return c / b + return div_trunc(c, b) } ` source_file := source.Source{path="test.bro", text=text} @@ -9392,6 +9406,313 @@ checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) { testing.expect(t, strings.contains(llvm_text, "divovf_trap")) } +@(test) +integer_slash_is_rejected_and_float_slash_remains_available :: proc(t: ^testing.T) { + Case :: struct {text, want: string} + invalid := []Case{ + {text=`main func() void { + a i32 = 4 + b i32 = 2 + _ = a / b +}`, want="integer '/' is not allowed"}, + {text=`main func() void { + a u32 = 4 + b u32 = 2 + _ = a / b +}`, want="integer '/' is not allowed"}, + {text=`main func() void { + _ = 4 / 2 +}`, want="integer '/' is not allowed"}, + {text=`main func() void { + values [4 / 2]u8 = undefined + _ = &values +}`, want="integer '/' is not allowed"}, + {text=`half func($value i32) i32 { return value / 2 } +main func() void { _ = $half(4) }`, want="integer '/' is not allowed"}, + {text=`main func() void { + value i32 = 8 + value /= 2 +}`, want="assign through an explicit division builtin"}, + } + for test_case in invalid { + source_file := source.Source{path="test.bro", text=test_case.text} + diagnostics := source.init_diagnostics(&source_file) + symbols := symbol.init_table() + stream := lexer.lex(&source_file, &diagnostics, &symbols) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, test_case.want) + } + testing.expect(t, found) + hir.destroy_module(&hir_module) + ast.destroy_module(&ast_module) + delete(stream.items) + symbol.destroy_table(&symbols) + source.destroy_diagnostics(&diagnostics) + } + + text := `main func() void { + value f32 = 5.0 / 2.0 + value /= 2.0 + _ = value +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + testing.expect_value(t, len(diagnostics.items), 0) +} + +@(test) +division_builtins_diagnose_arity_operands_and_comptime_failures :: proc(t: ^testing.T) { + text := `bad_arity :: div_floor(1) +bad_bool :: rem(true, false) +bad_family :: mod(i32(5), f32(3)) +zero_trunc :: div_trunc(1, 0) +zero_floor :: div_floor(1.0, 0.0) +zero_exact :: div_exact(1, 0) +zero_ceil :: div_ceil(1.0, 0.0) +zero_rem :: rem(1, 0) +zero_mod :: mod(1.0, 0.0) +inexact :: div_exact(5, 3) +overflow_trunc :: div_trunc(min_value(i32), -1) +overflow_floor :: div_floor(min_value(i32), -1) +overflow_exact :: div_exact(min_value(i32), -1) +overflow_ceil :: div_ceil(min_value(i32), -1) +main func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + zero_count := 0 + found_arity, found_operands, found_exact := false, false, false + overflow_count := 0 + for diagnostic in diagnostics.items { + found_arity = found_arity || strings.contains(diagnostic.message, "expects 2 arguments") + found_operands = found_operands || strings.contains(diagnostic.message, "compatible numeric operands") + found_exact = found_exact || strings.contains(diagnostic.message, "exact division has a remainder") + if strings.contains(diagnostic.message, "signed integer division overflow") { + overflow_count += 1 + } + if strings.contains(diagnostic.message, "division builtin denominator is zero") { + zero_count += 1 + } + } + testing.expect(t, found_arity) + testing.expect(t, found_operands) + testing.expect(t, found_exact) + testing.expect_value(t, overflow_count, 4) + testing.expect_value(t, zero_count, 6) +} + +@(test) +division_family_compiles_and_runs_for_integer_and_float_scalars :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-division-family" + main_path := "/tmp/brolang-test-division-family/main.bro" + output := "/tmp/brolang-test-division-family-output" + text := `COUNT :: div_exact(8, 2) +items [div_ceil(10, 3)]u8 :: [0, 0, 0, 0] +OPEN :: 5 +open_ceil i32 :: div_ceil(OPEN, 3) + +check_i32 func(a, b, qt, qf, qc, r, m i32) bool { + return div_trunc(a, b) == qt and div_floor(a, b) == qf and + div_ceil(a, b) == qc and rem(a, b) == r and mod(a, b) == m +} + +check_f32 func(a, b, qt, qf, qc, r, m f32) bool { + return div_trunc(a, b) == qt and div_floor(a, b) == qf and + div_ceil(a, b) == qc and rem(a, b) == r and mod(a, b) == m +} + +check_f64 func(a, b, qt, qf, qc, r, m f64) bool { + return div_trunc(a, b) == qt and div_floor(a, b) == qf and + div_ceil(a, b) == qc and rem(a, b) == r and mod(a, b) == m +} + +edge_rem func(a, b i32) i32 { return rem(a, b) } +edge_mod func(a, b i32) i32 { return mod(a, b) } + +main func() i32 { + if COUNT != 4 or items.len != 4 or open_ceil != 2 { return 1 } + if !check_i32(5, 3, 1, 1, 2, 2, 2) { return 2 } + if !check_i32(5, -3, -1, -2, -1, 2, -1) { return 3 } + if !check_i32(-5, 3, -1, -2, -1, -2, 1) { return 4 } + if !check_i32(-5, -3, 1, 1, 2, -2, -2) { return 5 } + if div_trunc(u32(5), u32(3)) != 1 or div_floor(u32(5), u32(3)) != 1 or + div_ceil(u32(5), u32(3)) != 2 or rem(u32(5), u32(3)) != 2 or mod(u32(5), u32(3)) != 2 { return 6 } + if div_exact(i32(6), i32(3)) != 2 or div_exact(u32(6), u32(3)) != 2 { return 7 } + if !check_f32(f32(5.0), f32(3.0), f32(1.0), f32(1.0), f32(2.0), f32(2.0), f32(2.0)) or + !check_f32(f32(5.0), f32(-3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(2.0), f32(-1.0)) or + !check_f32(f32(-5.0), f32(3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(-2.0), f32(1.0)) or + !check_f32(f32(-5.0), f32(-3.0), f32(1.0), f32(1.0), f32(2.0), f32(-2.0), f32(-2.0)) { return 8 } + if !check_f64(5.0, 3.0, 1.0, 1.0, 2.0, 2.0, 2.0) or + !check_f64(5.0, -3.0, -1.0, -2.0, -1.0, 2.0, -1.0) or + !check_f64(-5.0, 3.0, -1.0, -2.0, -1.0, -2.0, 1.0) or + !check_f64(-5.0, -3.0, 1.0, 1.0, 2.0, -2.0, -2.0) { return 9 } + if div_exact(f32(6.0), f32(3.0)) != 2.0 or div_exact(f64(6.0), f64(3.0)) != 2.0 { return 10 } + if edge_rem(-2147483648, -1) != 0 or edge_mod(-2147483648, -1) != 0 { return 11 } + return 0 +} +` + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + status := compiler_core.compile_package(directory, output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +division_builtins_trap_for_runtime_zero_overflow_and_inexact_results :: proc(t: ^testing.T) { + Case :: struct { + name: string, + type_name: string, + left: string, + right: string, + } + cases := [?]Case{ + {name="div_trunc", type_name="i32", left="1", right="0"}, + {name="div_floor", type_name="f32", left="f32(1.0)", right="f32(0.0)"}, + {name="div_exact", type_name="f64", left="1.0", right="0.0"}, + {name="div_ceil", type_name="i32", left="1", right="0"}, + {name="rem", type_name="f32", left="f32(1.0)", right="f32(0.0)"}, + {name="mod", type_name="f64", left="1.0", right="0.0"}, + {name="div_exact", type_name="i32", left="5", right="3"}, + {name="div_trunc", type_name="i32", left="-2147483648", right="-1"}, + {name="div_floor", type_name="i32", left="-2147483648", right="-1"}, + {name="div_exact", type_name="i32", left="-2147483648", right="-1"}, + {name="div_ceil", type_name="i32", left="-2147483648", right="-1"}, + } + for test_case, index in cases { + directory := fmt.aprintf("/tmp/brolang-test-division-trap-%d", index) + main_path := fmt.aprintf("%s/main.bro", directory) + output := fmt.aprintf("/tmp/brolang-test-division-trap-output-%d", index) + text := fmt.aprintf( + "invoke func(a, b %s) %s {{ return %s(a, b) }}\nmain func() void {{ _ = invoke(%s, %s) }}\n", + test_case.type_name, test_case.type_name, test_case.name, test_case.left, test_case.right, + ) + _ = os2.remove_all(directory) + _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + status := compiler_core.compile_package(directory, output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect(t, !state.success) + _ = os.remove(output) + _ = os2.remove_all(directory) + delete(text) + delete(output) + delete(main_path) + delete(directory) + } +} + +@(test) +qualified_division_builtin_names_resolve_as_package_functions :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-qualified-division" + math_directory := "/tmp/brolang-test-qualified-division/math" + app_directory := "/tmp/brolang-test-qualified-division/app" + math_path := "/tmp/brolang-test-qualified-division/math/math.bro" + main_path := "/tmp/brolang-test-qualified-division/app/main.bro" + output := "/tmp/brolang-test-qualified-division-output" + math_text := `div_floor func(a, b i32) i32 { return a + b } +` + main_text := `math :: import "../math" +main func() i32 { return math.div_floor(20, 22) } +` + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.make_directory(math_directory) == nil) + testing.expect(t, os.make_directory(app_directory) == nil) + testing.expect(t, os.write_entire_file(math_path, transmute([]byte)math_text)) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)main_text)) + status := compiler_core.compile_package(app_directory, output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 42) +} + +@(test) +division_builtins_emit_guards_rounding_and_single_integer_divisions :: proc(t: ^testing.T) { + text := `floor_i32 func(a, b i32) i32 { return div_floor(a, b) } +ceil_i32 func(a, b i32) i32 { return div_ceil(a, b) } +exact_i32 func(a, b i32) i32 { return div_exact(a, b) } +floor_u32 func(a, b u32) u32 { return div_floor(a, b) } +rem_i16 func(a, b i16) i16 { return rem(a, b) } +mod_i16 func(a, b i16) i16 { return mod(a, b) } +floor_f32 func(a, b f32) f32 { return div_floor(a, b) } +ceil_f64 func(a, b f64) f64 { return div_ceil(a, b) } +exact_f32 func(a, b f32) f32 { return div_exact(a, b) } +rem_f64 func(a, b f64) f64 { return rem(a, b) } +mod_f32 func(a, b f32) f32 { return mod(a, b) } +main func() void { + _ = floor_i32(5, 3) + _ = ceil_i32(5, 3) + _ = exact_i32(6, 3) + _ = floor_u32(5, 3) + _ = rem_i16(5, 3) + _ = mod_i16(5, 3) + _ = floor_f32(f32(5.0), f32(3.0)) + _ = ceil_f64(5.0, 3.0) + _ = exact_f32(f32(6.0), f32(3.0)) + _ = rem_f64(5.0, 3.0) + _ = mod_f32(f32(5.0), f32(3.0)) +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, strings.count(llvm_text, "sdiv i32"), 3) + testing.expect(t, !strings.contains(llvm_text, "srem i32")) + testing.expect(t, strings.contains(llvm_text, "udiv i32")) + testing.expect(t, strings.contains(llvm_text, "srem i16")) + testing.expect(t, strings.contains(llvm_text, "remspecial")) + testing.expect(t, strings.contains(llvm_text, "divzero_trap")) + testing.expect(t, strings.contains(llvm_text, "divovf_trap")) + testing.expect(t, strings.contains(llvm_text, "call float @llvm.floor.f32")) + testing.expect(t, strings.contains(llvm_text, "call double @llvm.ceil.f64")) + testing.expect(t, strings.contains(llvm_text, "call float @llvm.trunc.f32")) + testing.expect(t, strings.contains(llvm_text, "frem double")) +} + @(test) distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) { text := `Point :: struct { diff --git a/examples/programs/compound_assignment/main.bro b/examples/programs/compound_assignment/main.bro index 1dc442a..e4e4bb9 100644 --- a/examples/programs/compound_assignment/main.bro +++ b/examples/programs/compound_assignment/main.bro @@ -1,5 +1,4 @@ -# Milestone 6: compound assignment (`+=`, `-=`, `*=`, `/=`) and the binary -# arithmetic operators `-`, `*`, `/` with multiplicative precedence. +# Compound assignment (`+=`, `-=`, `*=`, `/=`) and explicit integer division. check_float func() i32 { x f64 = 10.0 @@ -15,7 +14,7 @@ check_float func() i32 { check_unsigned func() i32 { n u32 = 100 - n /= 7 # 14 (truncating integer division) + n = div_trunc(n, 7) # 14 n -= 4 # 10 if n == 10 { return 1 @@ -28,7 +27,7 @@ main func() i32 { total += 10 # 10 total -= 3 # 7 total *= 4 # 28 - total /= 2 # 14 + total = div_trunc(total, 2) # 14 # binary operators honour precedence: 14 + (2 * 3) - 4 == 16 total = total + 2 * 3 - 4 diff --git a/std/arraylist/arraylist.bro b/std/arraylist/arraylist.bro index 9ba3513..95cc061 100644 --- a/std/arraylist/arraylist.bro +++ b/std/arraylist/arraylist.bro @@ -30,7 +30,7 @@ reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem new_capacity usize = 8 if list.capacity >= 8 { - half usize :: list.capacity / 2 + half usize :: div_trunc(list.capacity, 2) if list.capacity > max_value(usize) - half { new_capacity = minimum_capacity } else { diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 2f3ad18..49450ed 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -61,7 +61,7 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { if element_size == 0 { return _empty_slice(T, count) } - if count > max_value(usize) / element_size { + if count > div_trunc(max_value(usize), element_size) { return .out_of_memory } @@ -86,7 +86,7 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu if element_size == 0 { return _empty_slice(T, new_count) } - if new_count > max_value(usize) / element_size { + if new_count > div_trunc(max_value(usize), element_size) { return .out_of_memory } @@ -125,7 +125,7 @@ _power_of_two func(value usize) bool { current usize = value while current > 1 { - half usize = current / 2 + half usize = div_trunc(current, 2) if half * 2 != current { return false }