enforce integer division via explicit builtins

This commit is contained in:
2026-07-13 11:39:06 +02:00
parent a4d0fb1e26
commit 2ed333c70d
13 changed files with 1004 additions and 98 deletions
+46 -2
View File
@@ -39,8 +39,8 @@ roadmap and milestone history.
### expressions and control flow ### expressions and control flow
- checked integer `+ - * /`, unary `-`, divide-by-zero traps, IEEE float arithmetic, comparisons, `!`, `and`, and `or` - checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or`
- assignments and compound assignments `+= -= *= /=` with single evaluation of complex lvalues - 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 - 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 - boolean `if` / `else if` / `else`, braceless single-statement branches, and optional parenthesized conditions
- `while` loops with optional post-iteration update clauses - `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 - fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks
- direct `return match ...` and `yield match ...` value-control-flow operands - 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 ### functions, C interop, and linking
- demand-monomorphized Brolang and C-ABI functions - demand-monomorphized Brolang and C-ABI functions
+1
View File
@@ -187,6 +187,7 @@ Current prototype features:
- Bodyless manual and imported C variadic declarations with default argument promotions - Bodyless manual and imported C variadic declarations with default argument promotions
- Ordered linking of additional C sources, objects, archives, and libraries - Ordered linking of additional C sources, objects, archives, and libraries
- Checked signed addition and unary negation - 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 - Static, eager runtime, mutable runtime, and deferred problematic globals
- Runtime diagnostics followed by `llvm.trap` - Runtime diagnostics followed by `llvm.trap`
+22 -9
View File
@@ -116,7 +116,7 @@
- `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized - `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 - 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); `*`/`/` - added the binary arithmetic operators `-`, `*`, `/` (previously only `+` existed); `*`/`/`
bind tighter than `+`/`-`, and prefix `-` (negation) is unchanged bind tighter than `+`/`-`, and prefix `-` (negation) is unchanged
- compound assignments preserve the target, operator, and right-hand side explicitly through - compound assignments preserve the target, operator, and right-hand side explicitly through
@@ -124,10 +124,9 @@
operation, and stores through that address operation, and stores through that address
- side-effecting index, field-base, and dereference expressions are evaluated once in - side-effecting index, field-base, and dereference expressions are evaluated once in
left-to-right order left-to-right order
- integer arithmetic traps on overflow (`Sub_Checked`/`Mul_Checked` via the LLVM - integer `+`, `-`, and `*` trap on overflow; milestone 32 later restricted `/` and `/=` to
`.with.overflow` intrinsics) and integer `/` traps on divide-by-zero and `INT_MIN / -1`; floats and introduced the explicit integer/float division family
floats follow IEEE (`fadd`/`fsub`/`fmul`/`fdiv`, no trap) - constant folding (global initializers) covers the arithmetic family
- constant folding (global initializers) covers `-`, `*`, `/` alongside `+`
7. enums (native and c interop) (implemented; see below) 7. enums (native and c interop) (implemented; see below)
- native enums are nominal value types with integer runtime representations - 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 - 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 inferred form where their arguments or result provide enough information
32. disallow arbitrary integer division 32. explicit division family (implemented)
- take inspiration from zig - `/` and `/=` are float-only; every integer use is rejected with guidance toward explicit
- see also below for a word on unchecked casts division, including literals, comptime execution, array counts, and compound assignment
- the user should be explicit about what they mean with integer division (e.g. `div`, `rem`) - 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 ## A word on unchecked casts
+183 -2
View File
@@ -399,6 +399,31 @@ Type_Builtin :: enum u8 {
Max_Value, 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 { 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) { if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None return .None
@@ -598,6 +623,10 @@ type_from_syntax :: proc(
changed = true changed = true
} }
} else { } 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") source.add(checker.diagnostics, span, "array count must be a compile-time integer expression")
return types.INVALID return types.INVALID
} }
@@ -2568,6 +2597,35 @@ infer_nested_expr :: proc(
return result 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( infer_compound_expr :: proc(
checker: ^Checker, checker: ^Checker,
expr: ast.Expr, expr: ast.Expr,
@@ -2938,6 +2996,11 @@ infer_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue 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 is_ptr_cast_call(checker, expr) {
if len(expr.args) != 2 { if len(expr.args) != 2 {
last = types.INVALID last = types.INVALID
@@ -3952,6 +4015,12 @@ record_demand :: proc(
right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file) right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file)
return left || right 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 return false
} }
@@ -4435,6 +4504,14 @@ build_constant_expr :: proc(
id := source.add(checker.diagnostics, expr.span, "division by zero in constant expression") id := source.add(checker.diagnostics, expr.span, "division by zero in constant expression")
return invalid_hir_expr(checker, expr.span, id, recovery_type) 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 || if constant.kind == .Overflow ||
(!types.is_concrete_integer(expected) && !fits_i64(constant.value)) { (!types.is_concrete_integer(expected) && !fits_i64(constant.value)) {
id := source.add( id := source.add(
@@ -4868,6 +4945,89 @@ build_nested_expr :: proc(
return result 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( fallible_aggregate :: proc(
checker: ^Checker, checker: ^Checker,
span: source.Span, span: source.Span,
@@ -5578,6 +5738,13 @@ build_binary_arith :: proc(
id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands") id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands")
return invalid_hir_expr(checker, span, id) 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 result_kind := hir.Expr_Kind.Add
#partial switch op { #partial switch op {
case .Sub: result_kind = .Sub case .Sub: result_kind = .Sub
@@ -5627,7 +5794,7 @@ build_expr :: proc(
expr := checker.ast_module.exprs[frame.expr] expr := checker.ast_module.exprs[frame.expr]
if frame.stage == 0 { if frame.stage == 0 {
constant := eval_constant(checker, frame.expr) 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) last = build_constant_expr(checker, expr, constant, frame.expected)
_ = pop(&stack) _ = pop(&stack)
continue continue
@@ -5807,6 +5974,13 @@ build_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue 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 is_ptr_cast_call(checker, expr) {
if len(expr.args) != 2 { if len(expr.args) != 2 {
id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args)) 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 rhs_type := checker.module.exprs[value].type
result_type := types.widest(target_type, rhs_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) { types.is_bool(result_type) {
id := source.add( id := source.add(
checker.diagnostics, checker.diagnostics,
+170 -24
View File
@@ -6,6 +6,8 @@ import "../source"
import "../symbol" import "../symbol"
import "../types" import "../types"
import "base:intrinsics" import "base:intrinsics"
import "core:math"
import "core:mem" import "core:mem"
COMPTIME_EVAL_QUOTA :: 100_000 COMPTIME_EVAL_QUOTA :: 100_000
@@ -28,6 +30,8 @@ Constant_Kind :: enum {
Value, Value,
Overflow, Overflow,
Div_By_Zero, Div_By_Zero,
Non_Exact,
Integer_Division,
} }
Constant :: struct { Constant :: struct {
@@ -94,8 +98,7 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant {
continue continue
} }
expr := checker.ast_module.exprs[frame.expr] expr := checker.ast_module.exprs[frame.expr]
if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && expr.kind != .Negate {
expr.kind != .Div && expr.kind != .Negate {
result := Constant{kind = .Not_Constant} result := Constant{kind = .Not_Constant}
if expr.kind == .Integer { if expr.kind == .Integer {
result = Constant{kind = .Value, value = i128(expr.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] operand = checker.constants[expr.left]
} }
result := Constant{kind = .Not_Constant} result := Constant{kind = .Not_Constant}
if operand.kind == .Div_By_Zero { if operand.kind == .Overflow {
result = Constant{kind = .Div_By_Zero}
} else if operand.kind == .Overflow {
result = Constant{kind = .Overflow} result = Constant{kind = .Overflow}
} else if operand.kind == .Value { } else if operand.kind == .Value {
value, overflow := intrinsics.overflow_sub(i128(0), operand.value) value, overflow := intrinsics.overflow_sub(i128(0), operand.value)
@@ -147,27 +148,17 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant {
right = checker.constants[expr.right] right = checker.constants[expr.right]
} }
result := Constant{kind = .Not_Constant} result := Constant{kind = .Not_Constant}
if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero { if left.kind == .Overflow || right.kind == .Overflow {
result = Constant{kind = .Div_By_Zero}
} else if left.kind == .Overflow || right.kind == .Overflow {
result = Constant{kind = .Overflow} result = Constant{kind = .Overflow}
} else if left.kind == .Value && right.kind == .Value { } else if left.kind == .Value && right.kind == .Value {
value: i128 value: i128
overflow: bool overflow: bool
div_by_zero: bool
#partial switch expr.kind { #partial switch expr.kind {
case .Sub: value, overflow = intrinsics.overflow_sub(left.value, right.value) case .Sub: value, overflow = intrinsics.overflow_sub(left.value, right.value)
case .Mul: value, overflow = intrinsics.overflow_mul(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) case: value, overflow = intrinsics.overflow_add(left.value, right.value)
} }
switch { switch {
case div_by_zero: result = Constant{kind = .Div_By_Zero}
case overflow: result = Constant{kind = .Overflow} case overflow: result = Constant{kind = .Overflow}
case: result = Constant{kind = .Value, value = value} case: result = Constant{kind = .Value, value = value}
} }
@@ -226,6 +217,8 @@ Ct_Error_Kind :: enum u8 {
Not_Comptime, Not_Comptime,
Overflow, Overflow,
Div_By_Zero, Div_By_Zero,
Non_Exact,
Integer_Division,
Quota, Quota,
} }
@@ -1105,7 +1098,8 @@ ct_eval_expr :: proc(
} }
return ct_eval_unary(state, expr.kind, value, expr.span) return ct_eval_unary(state, expr.kind, value, expr.span)
case .Add, .Sub, .Mul, .Div, .Eq, .Ne, .Lt, .Le, .Gt, .Ge: 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 { if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok 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 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 value: i128
overflow := false overflow := false
#partial switch op { #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) value, overflow = intrinsics.overflow_sub(left.integer, right.integer)
case .Mul: case .Mul:
value, overflow = intrinsics.overflow_mul(left.integer, right.integer) 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: case:
value, overflow = intrinsics.overflow_add(left.integer, right.integer) 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") 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) { 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) { if id == INVALID_CT_VALUE || int(id) >= len(state.values) {
return INVALID_CT_VALUE, ct_flow(.Normal), false 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 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 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) target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false)
if !available { if !available {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package") 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} return Constant{kind=.Overflow}
case .Div_By_Zero: case .Div_By_Zero:
return Constant{kind=.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} return Constant{kind=.Not_Constant}
} }
@@ -2927,6 +3059,10 @@ eval_comptime_statements :: proc(
return Constant{kind=.Overflow}, false, false return Constant{kind=.Overflow}, false, false
case .Div_By_Zero: case .Div_By_Zero:
return Constant{kind=.Div_By_Zero}, false, false 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 return Constant{kind=.Not_Constant}, false, false
} }
@@ -2958,6 +3094,10 @@ eval_comptime_call :: proc(
return Constant{kind=.Overflow} return Constant{kind=.Overflow}
case .Div_By_Zero: case .Div_By_Zero:
return Constant{kind=.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} 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 !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.I64
} }
return types.INVALID return types.INVALID
@@ -3038,6 +3178,12 @@ build_comptime_expr :: proc(
if state.error == .Overflow { if state.error == .Overflow {
return build_constant_expr(checker, expr, Constant{kind=.Overflow}, expected) 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 diagnostic := state.diagnostic
if diagnostic == source.INVALID_DIAGNOSTIC { if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = source.add(checker.diagnostics, expr.span, "expression cannot be evaluated at comptime") diagnostic = source.add(checker.diagnostics, expr.span, "expression cannot be evaluated at comptime")
+6
View File
@@ -113,6 +113,12 @@ Expr_Kind :: enum u8 {
Sub, Sub,
Mul, Mul,
Div, Div,
Div_Trunc,
Div_Floor,
Div_Exact,
Div_Ceil,
Rem,
Mod,
Pointer_Add, Pointer_Add,
Eq, Eq,
Ne, Ne,
+6
View File
@@ -109,6 +109,12 @@ Opcode :: enum u8 {
Sub_Checked, Sub_Checked,
Mul_Checked, Mul_Checked,
Div_Checked, Div_Checked,
Div_Trunc_Checked,
Div_Floor_Checked,
Div_Exact_Checked,
Div_Ceil_Checked,
Rem_Checked,
Mod_Checked,
Pointer_Add, Pointer_Add,
Not, Not,
Compare, Compare,
+213 -31
View File
@@ -257,7 +257,10 @@ valid_value :: proc(
.Fallible_Error, .Extract, .Select, .Unwrap, .Fallible_Error, .Extract, .Select, .Unwrap,
.Optional_Is_Some, .Optional_Value, .Orelse, .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, .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 return true
case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin,
.Store, .Fill, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: .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) fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_index)
} }
// emit_checked_division emits a trapping integer division guarding divide-by-zero emit_division_zero_guard :: proc(
// and signed `INT_MIN / -1` overflow, or a plain floating-point division.
emit_checked_division :: proc(
emitter: ^Emitter, emitter: ^Emitter,
instructions: []ir.Instruction, instructions: []ir.Instruction,
instruction_index: int, instruction_index: int,
@@ -592,56 +593,225 @@ emit_checked_division :: proc(
) { ) {
type_name := llvm_type(instruction.type, &emitter.module.types) type_name := llvm_type(instruction.type, &emitter.module.types)
if types.is_float(instruction.type, emitter.module.target) { if types.is_float(instruction.type, emitter.module.target) {
fmt.sbprintf(&emitter.builder, " %%v%d = fdiv %s ", instruction_index, type_name) fmt.sbprintf(&emitter.builder, " %%divzero%d = fcmp oeq %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) write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types)
strings.write_string(&emitter.builder, "\n") strings.write_string(&emitter.builder, ", 0.000000e+00\n")
return } else {
}
signed := !types.is_unsigned(instruction.type, emitter.module.target)
fmt.sbprintf(&emitter.builder, " %%divzero%d = icmp eq %s ", instruction_index, type_name) 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) write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types)
strings.write_string(&emitter.builder, ", 0\n") strings.write_string(&emitter.builder, ", 0\n")
}
fmt.sbprintf( fmt.sbprintf(
&emitter.builder, &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, instruction_index,
instruction_index, instruction_index,
) )
fmt.sbprintf(&emitter.builder, "divzero_trap%d:\n", instruction_index) message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "division builtin denominator is zero")
zero_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "integer division by zero") emit_trap_call(emitter, message)
emit_trap_call(emitter, zero_message)
fmt.sbprintf(&emitter.builder, " unreachable\ndivzero_ok%d:\n", instruction_index) 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))
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) 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) write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", %d\n", min_value) fmt.sbprintf(&emitter.builder, ", %d\n %%divminhi%d = icmp eq %s ", min_value, instruction_index, type_name)
fmt.sbprintf(&emitter.builder, " %%divminhi%d = icmp eq %s ", instruction_index, type_name)
write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) 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( fmt.sbprintf(
&emitter.builder, &emitter.builder,
" br i1 %%divovf%d, label %%divovf_trap%d, label %%divovf_ok%d\n", ", -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, instruction_index,
instruction_index, instruction_index,
) )
fmt.sbprintf(&emitter.builder, "divovf_trap%d:\n", instruction_index) message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "signed integer division overflow")
ovf_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "signed integer division overflow") emit_trap_call(emitter, message)
emit_trap_call(emitter, ovf_message)
fmt.sbprintf(&emitter.builder, " unreachable\ndivovf_ok%d:\n", instruction_index) 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) 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) write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types)
strings.write_string(&emitter.builder, ", ") strings.write_string(&emitter.builder, ", ")
write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types)
strings.write_string(&emitter.builder, "\n") 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( 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") emit_checked_arithmetic(emitter, instructions, instruction_index, instruction, "mul", "fmul", "integer multiplication overflow")
case .Div_Checked: case .Div_Checked:
if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) || 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") emit_recovery_value(emitter, instruction_index, instruction, "invalid division operand")
continue 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: case .Pointer_Add:
result_item, result_ok := types.node(&emitter.module.types, instruction.type) 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 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) { 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 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} widths := [?]int{8, 16, 32, 64}
overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"} overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"}
for bits in widths { for bits in widths {
+8 -1
View File
@@ -681,7 +681,8 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
case .Negate: case .Negate:
stack[frame_index].stage = 5 stack[frame_index].stage = 5
append(&stack, Lower_Expr_Frame{expr=expr.left}) 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 stack[frame_index].stage = 2
append(&stack, Lower_Expr_Frame{expr=expr.left}) append(&stack, Lower_Expr_Frame{expr=expr.left})
case .Call: 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 .Sub: op = .Sub_Checked
case .Mul: op = .Mul_Checked case .Mul: op = .Mul_Checked
case .Div: op = .Div_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 case .Pointer_Add: op = .Pointer_Add
} }
last = append_instruction(state, ir.Instruction{ last = append_instruction(state, ir.Instruction{
+331 -10
View File
@@ -6246,7 +6246,7 @@ main func() void {}
@(test) @(test)
constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) { 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 {} main func() void {}
` `
source_file := source.Source{path="test.bro", text=text} source_file := source.Source{path="test.bro", text=text}
@@ -6265,7 +6265,7 @@ main func() void {}
found_overflow := false found_overflow := false
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
found_division_by_zero = found_division_by_zero || 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 || found_overflow = found_overflow ||
strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range") 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) { malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) {
module := ir.init_module() module := ir.init_module()
defer ir.destroy_module(&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[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[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[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{ append(&module.functions, ir.Function{
link_name=strings.clone("main"), link_name=strings.clone("main"),
calling_convention=.C, 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, "%v0 = add i8 0, -86"))
testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766")) testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766"))
testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846")) 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, "@bro.trap(ptr %message, i64 %length) noreturn"))
testing.expect(t, !strings.contains(text, "%v-1")) testing.expect(t, !strings.contains(text, "%v-1"))
llvm_path := "/tmp/brolang-test-malformed-recovery.ll" 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 += 6
signed -= 2 signed -= 2
signed *= 3 signed *= 3
signed /= 4 signed = div_trunc(signed, 4)
unsigned u32 = 24 unsigned u32 = 24
unsigned += 6 unsigned += 6
unsigned -= 2 unsigned -= 2
unsigned *= 3 unsigned *= 3
unsigned /= 4 unsigned = div_trunc(unsigned, 4)
real f64 = 24.0 real f64 = 24.0
real += 6.0 real += 6.0
real -= 2.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[.Add], 3)
testing.expect_value(t, operation_counts[.Sub], 3) testing.expect_value(t, operation_counts[.Sub], 3)
testing.expect_value(t, operation_counts[.Mul], 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 add_count := 0
sub_count := 0 sub_count := 0
mul_count := 0 mul_count := 0
div_count := 0 div_count := 0
div_trunc_count := 0
for instruction in ir_module.functions[0].instructions { for instruction in ir_module.functions[0].instructions {
#partial switch instruction.op { #partial switch instruction.op {
case .Add_Checked: add_count += 1 case .Add_Checked: add_count += 1
case .Sub_Checked: sub_count += 1 case .Sub_Checked: sub_count += 1
case .Mul_Checked: mul_count += 1 case .Mul_Checked: mul_count += 1
case .Div_Checked: div_count += 1 case .Div_Checked: div_count += 1
case .Div_Trunc_Checked: div_trunc_count += 1
case: case:
} }
} }
testing.expect_value(t, add_count, 3) testing.expect_value(t, add_count, 3)
testing.expect_value(t, sub_count, 3) testing.expect_value(t, sub_count, 3)
testing.expect_value(t, mul_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) @(test)
@@ -9310,7 +9324,7 @@ binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) {
text := `main func() i32 { text := `main func() i32 {
a i32 = 1 a i32 = 1
b u32 = 2 b u32 = 2
_ = a / b _ = a + b
return 0 return 0
} }
` `
@@ -9366,7 +9380,7 @@ checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) {
a i32 = 10 a i32 = 10
b i32 = 3 b i32 = 3
c i32 = a - b c i32 = a - b
return c / b return div_trunc(c, b)
} }
` `
source_file := source.Source{path="test.bro", text=text} 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")) 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) @(test)
distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) { distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) {
text := `Point :: struct { text := `Point :: struct {
@@ -1,5 +1,4 @@
# Milestone 6: compound assignment (`+=`, `-=`, `*=`, `/=`) and the binary # Compound assignment (`+=`, `-=`, `*=`, `/=`) and explicit integer division.
# arithmetic operators `-`, `*`, `/` with multiplicative precedence.
check_float func() i32 { check_float func() i32 {
x f64 = 10.0 x f64 = 10.0
@@ -15,7 +14,7 @@ check_float func() i32 {
check_unsigned func() i32 { check_unsigned func() i32 {
n u32 = 100 n u32 = 100
n /= 7 # 14 (truncating integer division) n = div_trunc(n, 7) # 14
n -= 4 # 10 n -= 4 # 10
if n == 10 { if n == 10 {
return 1 return 1
@@ -28,7 +27,7 @@ main func() i32 {
total += 10 # 10 total += 10 # 10
total -= 3 # 7 total -= 3 # 7
total *= 4 # 28 total *= 4 # 28
total /= 2 # 14 total = div_trunc(total, 2) # 14
# binary operators honour precedence: 14 + (2 * 3) - 4 == 16 # binary operators honour precedence: 14 + (2 * 3) - 4 == 16
total = total + 2 * 3 - 4 total = total + 2 * 3 - 4
+1 -1
View File
@@ -30,7 +30,7 @@ reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem
new_capacity usize = 8 new_capacity usize = 8
if list.capacity >= 8 { if list.capacity >= 8 {
half usize :: list.capacity / 2 half usize :: div_trunc(list.capacity, 2)
if list.capacity > max_value(usize) - half { if list.capacity > max_value(usize) - half {
new_capacity = minimum_capacity new_capacity = minimum_capacity
} else { } else {
+3 -3
View File
@@ -61,7 +61,7 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
if element_size == 0 { if element_size == 0 {
return _empty_slice(T, count) 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 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 { if element_size == 0 {
return _empty_slice(T, new_count) 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 return .out_of_memory
} }
@@ -125,7 +125,7 @@ _power_of_two func(value usize) bool {
current usize = value current usize = value
while current > 1 { while current > 1 {
half usize = current / 2 half usize = div_trunc(current, 2)
if half * 2 != current { if half * 2 != current {
return false return false
} }