enforce integer division via explicit builtins
This commit is contained in:
+46
-2
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+172
-26
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+222
-40
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
+331
-10
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user