comptime eval
This commit is contained in:
+2
-1
@@ -54,6 +54,7 @@ roadmap and milestone history.
|
|||||||
- demand-monomorphized Brolang and C-ABI functions
|
- demand-monomorphized Brolang and C-ABI functions
|
||||||
- integer comptime value parameters such as `make_array func($N usize) [N]u8`, specialized by value and omitted from the runtime ABI
|
- integer comptime value parameters such as `make_array func($N usize) [N]u8`, specialized by value and omitted from the runtime ABI
|
||||||
- explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI
|
- explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI
|
||||||
|
- forced comptime expressions such as `$sum(1, 2)` and comptime value blocks such as `${ yield 4 }` for integer constant contexts
|
||||||
- bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names
|
- bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names
|
||||||
- concrete-only C signatures, C variadic declarations/calls, and C default argument promotions
|
- concrete-only C signatures, C variadic declarations/calls, and C default argument promotions
|
||||||
- Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns
|
- Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns
|
||||||
@@ -81,7 +82,7 @@ roadmap and milestone history.
|
|||||||
|
|
||||||
## PLANNED / DEFERRED
|
## PLANNED / DEFERRED
|
||||||
|
|
||||||
- comptime-evaluable functions
|
- broader Zig-style comptime execution
|
||||||
- tuples and native Brolang variadic functions
|
- tuples and native Brolang variadic functions
|
||||||
- exporting Brolang functions to C and broader target-specific C ABI lowering
|
- exporting Brolang functions to C and broader target-specific C ABI lowering
|
||||||
- non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments
|
- non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ Current prototype features:
|
|||||||
- Qualified imported globals and functions with package-aware symbol mangling
|
- Qualified imported globals and functions with package-aware symbol mangling
|
||||||
- Demand-monomorphized Brolang and C-ABI functions
|
- Demand-monomorphized Brolang and C-ABI functions
|
||||||
- Integer and type comptime parameters (`func($N usize) [N]u8`, `func($T type, value T) T`) specialized by comptime argument
|
- Integer and type comptime parameters (`func($N usize) [N]u8`, `func($T type, value T) T`) specialized by comptime argument
|
||||||
|
- Forced comptime expressions (`$sum(1, 2)`) and comptime value blocks (`${ yield 4 }`) for integer constant contexts
|
||||||
- Bodyless concrete C function declarations with exact external symbol names
|
- Bodyless concrete C function declarations with exact external symbol names
|
||||||
- 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
|
||||||
|
|||||||
@@ -651,7 +651,29 @@
|
|||||||
inferred type parameters, first-class type values, or comptime execution
|
inferred type parameters, first-class type values, or comptime execution
|
||||||
|
|
||||||
27.6 comptime-evaluable constants/functions
|
27.6 comptime-evaluable constants/functions
|
||||||
- planned shape: `$x :: 32` and `$sum func(a, b int) int { ... }`
|
- `$expr` forces comptime evaluation of an expression:
|
||||||
|
`x :: $32`, `n :: $sum(1, 2)`, and `res :: ${ ... }`
|
||||||
|
- constant contexts such as array counts and comptime value arguments implicitly
|
||||||
|
require comptime evaluation; ordinary immutable bindings remain ordinary bindings
|
||||||
|
- ordinary `func` calls are comptime-evaluable when reached from a comptime context;
|
||||||
|
do not add a separate `$sum func(...)` declaration form
|
||||||
|
- v1 evaluator supports integer literals/arithmetic, boolean conditions, immutable
|
||||||
|
locals, `return`, `if`/`else`, comptime blocks, and direct calls to other evaluable
|
||||||
|
brolang functions
|
||||||
|
- broader Zig-style comptime execution is milestone 27.7
|
||||||
|
|
||||||
|
27.7 broader Zig-style comptime execution
|
||||||
|
- extend the comptime evaluator from integer scalars into a real compile-time value
|
||||||
|
model: bools, floats, strings, arrays/slices, structs/unions/enums, optionals,
|
||||||
|
fallibles, and pointers to comptime storage
|
||||||
|
- support mutable comptime locals/assignment, loops with an evaluation quota,
|
||||||
|
`defer`, `match`, value blocks/`yield`, and `try`/`catch`
|
||||||
|
- allow calls through comptime-known function values/function pointers; keep external
|
||||||
|
`c_func` calls runtime-only unless a future compiler intrinsic explicitly models
|
||||||
|
their behavior
|
||||||
|
- immutable locals/globals with comptime-known initializers may feed comptime
|
||||||
|
evaluation; runtime-dependent values remain invalid in comptime contexts
|
||||||
|
- no runtime side effects during comptime evaluation
|
||||||
|
|
||||||
28. brolang build system (requires comptime execution)
|
28. brolang build system (requires comptime execution)
|
||||||
|
|
||||||
@@ -1244,9 +1266,10 @@ data :: read_file(path) catch |e| match e {
|
|||||||
|
|
||||||
```
|
```
|
||||||
make_array func($N usize) [N]u8 { ... } # implemented: integer comptime value params
|
make_array func($N usize) [N]u8 { ... } # implemented: integer comptime value params
|
||||||
max func($T type, a, b T) T { ... } # deferred: comptime type params
|
max func($T type, a, b T) T { ... } # implemented: comptime type params
|
||||||
$x :: 32 # deferred: comptime evaluable constant
|
x :: $32 # implemented: force comptime expression evaluation
|
||||||
$sum func(a, b int) int { ... } # deferred: comptime evaluable function
|
n :: $sum(1, 2) # implemented: ordinary functions can run at comptime
|
||||||
|
res :: ${ yield 4 } # implemented: comptime value block
|
||||||
```
|
```
|
||||||
|
|
||||||
## A word on memory allocation
|
## A word on memory allocation
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ Expr_Kind :: enum u8 {
|
|||||||
Struct_Literal,
|
Struct_Literal,
|
||||||
Keyed,
|
Keyed,
|
||||||
Cast,
|
Cast,
|
||||||
|
Comptime,
|
||||||
Negate,
|
Negate,
|
||||||
Not,
|
Not,
|
||||||
Add,
|
Add,
|
||||||
|
|||||||
+228
-12
@@ -389,6 +389,7 @@ eval_integer_constant_in_context :: proc(
|
|||||||
pkg: ast.Package_Id,
|
pkg: ast.Package_Id,
|
||||||
file: ast.File_Id,
|
file: ast.File_Id,
|
||||||
depth := 0,
|
depth := 0,
|
||||||
|
values: []Comptime_Value = nil,
|
||||||
) -> Constant {
|
) -> Constant {
|
||||||
if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
return Constant{kind = .Not_Constant}
|
return Constant{kind = .Not_Constant}
|
||||||
@@ -397,8 +398,15 @@ eval_integer_constant_in_context :: proc(
|
|||||||
#partial switch expr.kind {
|
#partial switch expr.kind {
|
||||||
case .Integer:
|
case .Integer:
|
||||||
return Constant{kind = .Value, value = i128(expr.integer)}
|
return Constant{kind = .Value, value = i128(expr.integer)}
|
||||||
|
case .Bool:
|
||||||
|
return Constant{kind = .Value, value = i128(expr.integer)}
|
||||||
case .Name:
|
case .Name:
|
||||||
if !symbol.is_valid(expr.qualifier) {
|
if !symbol.is_valid(expr.qualifier) {
|
||||||
|
if value, ok := find_comptime_value(values, expr.name); ok {
|
||||||
|
if value.kind == .Integer {
|
||||||
|
return Constant{kind = .Value, value = value.value}
|
||||||
|
}
|
||||||
|
}
|
||||||
if value, ok := current_comptime_value(checker, expr.name); ok {
|
if value, ok := current_comptime_value(checker, expr.name); ok {
|
||||||
if value.kind == .Integer {
|
if value.kind == .Integer {
|
||||||
return Constant{kind = .Value, value = value.value}
|
return Constant{kind = .Value, value = value.value}
|
||||||
@@ -417,17 +425,32 @@ eval_integer_constant_in_context :: proc(
|
|||||||
if g.external || !g.immutable {
|
if g.external || !g.immutable {
|
||||||
return Constant{kind = .Not_Constant}
|
return Constant{kind = .Not_Constant}
|
||||||
}
|
}
|
||||||
return eval_integer_constant_in_context(checker, g.expr, g.pkg, g.file, depth+1)
|
return eval_integer_constant_in_context(checker, g.expr, g.pkg, g.file, depth+1, values)
|
||||||
|
case .Comptime:
|
||||||
|
if expr.left != ast.INVALID_EXPR {
|
||||||
|
return eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values)
|
||||||
|
}
|
||||||
|
result, produced, ok := eval_comptime_statements(checker, expr.body, pkg, file, depth+1, values, true)
|
||||||
|
if ok && produced {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return Constant{kind = .Not_Constant}
|
||||||
case .Negate:
|
case .Negate:
|
||||||
operand := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1)
|
operand := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values)
|
||||||
if operand.kind == .Value {
|
if operand.kind == .Value {
|
||||||
value, overflow := intrinsics.overflow_sub(i128(0), operand.value)
|
value, overflow := intrinsics.overflow_sub(i128(0), operand.value)
|
||||||
return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value}
|
return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value}
|
||||||
}
|
}
|
||||||
return operand
|
return operand
|
||||||
|
case .Not:
|
||||||
|
operand := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values)
|
||||||
|
if operand.kind == .Value {
|
||||||
|
return Constant{kind = .Value, value = 1 if operand.value == 0 else 0}
|
||||||
|
}
|
||||||
|
return operand
|
||||||
case .Add, .Sub, .Mul, .Div:
|
case .Add, .Sub, .Mul, .Div:
|
||||||
left := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1)
|
left := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values)
|
||||||
right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1)
|
right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1, values)
|
||||||
if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero {
|
if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero {
|
||||||
return Constant{kind = .Div_By_Zero}
|
return Constant{kind = .Div_By_Zero}
|
||||||
}
|
}
|
||||||
@@ -453,8 +476,161 @@ eval_integer_constant_in_context :: proc(
|
|||||||
value, overflow = intrinsics.overflow_add(left.value, right.value)
|
value, overflow = intrinsics.overflow_add(left.value, right.value)
|
||||||
}
|
}
|
||||||
return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value}
|
return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value}
|
||||||
|
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge:
|
||||||
|
left := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values)
|
||||||
|
right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1, values)
|
||||||
|
if left.kind != .Value || right.kind != .Value {
|
||||||
|
if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero {
|
||||||
|
return Constant{kind = .Div_By_Zero}
|
||||||
|
}
|
||||||
|
if left.kind == .Overflow || right.kind == .Overflow {
|
||||||
|
return Constant{kind = .Overflow}
|
||||||
}
|
}
|
||||||
return Constant{kind = .Not_Constant}
|
return Constant{kind = .Not_Constant}
|
||||||
|
}
|
||||||
|
ok := false
|
||||||
|
#partial switch expr.kind {
|
||||||
|
case .Eq: ok = left.value == right.value
|
||||||
|
case .Ne: ok = left.value != right.value
|
||||||
|
case .Lt: ok = left.value < right.value
|
||||||
|
case .Le: ok = left.value <= right.value
|
||||||
|
case .Gt: ok = left.value > right.value
|
||||||
|
case .Ge: ok = left.value >= right.value
|
||||||
|
}
|
||||||
|
return Constant{kind = .Value, value = 1 if ok else 0}
|
||||||
|
case .And, .Or:
|
||||||
|
left := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values)
|
||||||
|
if left.kind != .Value {
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
if expr.kind == .And && left.value == 0 {
|
||||||
|
return Constant{kind = .Value, value = 0}
|
||||||
|
}
|
||||||
|
if expr.kind == .Or && left.value != 0 {
|
||||||
|
return Constant{kind = .Value, value = 1}
|
||||||
|
}
|
||||||
|
right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1, values)
|
||||||
|
if right.kind == .Value {
|
||||||
|
return Constant{kind = .Value, value = 1 if right.value != 0 else 0}
|
||||||
|
}
|
||||||
|
return right
|
||||||
|
case .Call:
|
||||||
|
return eval_comptime_call(checker, expr, pkg, file, depth+1, values)
|
||||||
|
}
|
||||||
|
return Constant{kind = .Not_Constant}
|
||||||
|
}
|
||||||
|
|
||||||
|
eval_comptime_statements :: proc(
|
||||||
|
checker: ^Checker,
|
||||||
|
statements: []ast.Stmt_Id,
|
||||||
|
pkg: ast.Package_Id,
|
||||||
|
file: ast.File_Id,
|
||||||
|
depth: int,
|
||||||
|
values: []Comptime_Value,
|
||||||
|
yield_returns: bool,
|
||||||
|
) -> (Constant, bool, bool) {
|
||||||
|
env: [dynamic]Comptime_Value
|
||||||
|
env.allocator = checker.allocator
|
||||||
|
append(&env, ..values)
|
||||||
|
defer delete(env)
|
||||||
|
for statement_id in statements {
|
||||||
|
if statement_id == ast.INVALID_STMT || int(statement_id) >= len(checker.ast_module.statements) {
|
||||||
|
return Constant{kind=.Not_Constant}, false, false
|
||||||
|
}
|
||||||
|
statement := checker.ast_module.statements[statement_id]
|
||||||
|
#partial switch statement.kind {
|
||||||
|
case .Declaration:
|
||||||
|
if statement.expr == ast.INVALID_EXPR || !statement.immutable {
|
||||||
|
return Constant{kind=.Not_Constant}, false, false
|
||||||
|
}
|
||||||
|
value := eval_integer_constant_in_context(checker, statement.expr, pkg, file, depth+1, env[:])
|
||||||
|
if value.kind != .Value || statement.name == checker.sink_symbol {
|
||||||
|
return value, false, false
|
||||||
|
}
|
||||||
|
append(&env, Comptime_Value{name=statement.name, type=types.I64, value=value.value})
|
||||||
|
case .Return:
|
||||||
|
if yield_returns || statement.expr == ast.INVALID_EXPR {
|
||||||
|
return Constant{kind=.Not_Constant}, false, false
|
||||||
|
}
|
||||||
|
value := eval_integer_constant_in_context(checker, statement.expr, pkg, file, depth+1, env[:])
|
||||||
|
return value, value.kind == .Value, value.kind == .Value
|
||||||
|
case .Yield:
|
||||||
|
if !yield_returns || statement.expr == ast.INVALID_EXPR {
|
||||||
|
return Constant{kind=.Not_Constant}, false, false
|
||||||
|
}
|
||||||
|
value := eval_integer_constant_in_context(checker, statement.expr, pkg, file, depth+1, env[:])
|
||||||
|
return value, value.kind == .Value, value.kind == .Value
|
||||||
|
case .If:
|
||||||
|
if len(statement.captures) > 0 || statement.guard != ast.INVALID_EXPR {
|
||||||
|
return Constant{kind=.Not_Constant}, false, false
|
||||||
|
}
|
||||||
|
condition := eval_integer_constant_in_context(checker, statement.expr, pkg, file, depth+1, env[:])
|
||||||
|
if condition.kind != .Value {
|
||||||
|
return condition, false, false
|
||||||
|
}
|
||||||
|
body := statement.body if condition.value != 0 else statement.else_body
|
||||||
|
value, produced, ok := eval_comptime_statements(checker, body, pkg, file, depth+1, env[:], yield_returns)
|
||||||
|
if !ok || produced {
|
||||||
|
return value, produced, ok
|
||||||
|
}
|
||||||
|
case:
|
||||||
|
return Constant{kind=.Not_Constant}, false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Constant{kind=.Not_Constant}, false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
eval_comptime_call :: proc(
|
||||||
|
checker: ^Checker,
|
||||||
|
expr: ast.Expr,
|
||||||
|
pkg: ast.Package_Id,
|
||||||
|
file: ast.File_Id,
|
||||||
|
depth: int,
|
||||||
|
values: []Comptime_Value,
|
||||||
|
) -> Constant {
|
||||||
|
if expr.left != ast.INVALID_EXPR || depth > 64 {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
target_pkg, available := expr_package(checker, expr, pkg, file, false)
|
||||||
|
if !available {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
template := find_template(checker, expr.name, target_pkg)
|
||||||
|
if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
function := checker.ast_module.functions[template]
|
||||||
|
if function.c_abi || !function.has_body || types.is_valid(function.error) ||
|
||||||
|
len(function.unsupported_reason) > 0 || !valid_call_arity(function, len(expr.args)) {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
comptime_values, comptime_ok := collect_comptime_values(checker, function, expr.args, pkg, file, false, values)
|
||||||
|
defer delete(comptime_values, checker.allocator)
|
||||||
|
if !comptime_ok {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
env: [dynamic]Comptime_Value
|
||||||
|
env.allocator = checker.allocator
|
||||||
|
defer delete(env)
|
||||||
|
append(&env, ..comptime_values)
|
||||||
|
for param, index in function.params {
|
||||||
|
if param.comptime_value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if index >= len(expr.args) {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
value := eval_integer_constant_in_context(checker, expr.args[index], pkg, file, depth+1, values)
|
||||||
|
if value.kind != .Value {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
append(&env, Comptime_Value{name=param.name, type=types.I64, value=value.value})
|
||||||
|
}
|
||||||
|
result, produced, ok := eval_comptime_statements(checker, function.body, function.pkg, function.file, depth+1, env[:], false)
|
||||||
|
if !ok || !produced {
|
||||||
|
return Constant{kind=.Not_Constant}
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fits_signed_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool {
|
fits_signed_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool {
|
||||||
@@ -952,6 +1128,7 @@ collect_comptime_values :: proc(
|
|||||||
pkg: ast.Package_Id,
|
pkg: ast.Package_Id,
|
||||||
file: ast.File_Id,
|
file: ast.File_Id,
|
||||||
diagnose := false,
|
diagnose := false,
|
||||||
|
extra_values: []Comptime_Value = nil,
|
||||||
) -> ([]Comptime_Value, bool) {
|
) -> ([]Comptime_Value, bool) {
|
||||||
if !function_has_comptime_params(function) {
|
if !function_has_comptime_params(function) {
|
||||||
return nil, true
|
return nil, true
|
||||||
@@ -1002,7 +1179,7 @@ collect_comptime_values :: proc(
|
|||||||
}
|
}
|
||||||
constant := Constant{kind = .Not_Constant}
|
constant := Constant{kind = .Not_Constant}
|
||||||
if index < len(args) {
|
if index < len(args) {
|
||||||
constant = eval_integer_constant_in_context(checker, args[index], pkg, file)
|
constant = eval_integer_constant_in_context(checker, args[index], pkg, file, values=extra_values)
|
||||||
}
|
}
|
||||||
if constant.kind != .Value {
|
if constant.kind != .Value {
|
||||||
if diagnose {
|
if diagnose {
|
||||||
@@ -1145,6 +1322,11 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
|
|||||||
}
|
}
|
||||||
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast:
|
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast:
|
||||||
append(&stack, expr.left)
|
append(&stack, expr.left)
|
||||||
|
case .Comptime:
|
||||||
|
if expr.left != ast.INVALID_EXPR {
|
||||||
|
append(&stack, expr.left)
|
||||||
|
}
|
||||||
|
mark_block_imports_used(checker, expr.body, file)
|
||||||
case .Catch:
|
case .Catch:
|
||||||
append(&stack, expr.left)
|
append(&stack, expr.left)
|
||||||
if expr.right != ast.INVALID_EXPR {
|
if expr.right != ast.INVALID_EXPR {
|
||||||
@@ -1744,6 +1926,25 @@ infer_compound_expr :: proc(
|
|||||||
) -> types.Type {
|
) -> types.Type {
|
||||||
store := &checker.module.types
|
store := &checker.module.types
|
||||||
#partial switch expr.kind {
|
#partial switch expr.kind {
|
||||||
|
case .Comptime:
|
||||||
|
constant := Constant{kind=.Not_Constant}
|
||||||
|
if expr.left != ast.INVALID_EXPR {
|
||||||
|
constant = eval_integer_constant_in_context(checker, expr.left, pkg, file)
|
||||||
|
}
|
||||||
|
if expr.left == ast.INVALID_EXPR {
|
||||||
|
value, produced, ok := eval_comptime_statements(checker, expr.body, pkg, file, 0, nil, true)
|
||||||
|
if ok && produced {
|
||||||
|
constant = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if constant.kind == .Overflow || constant.kind == .Div_By_Zero ||
|
||||||
|
(constant.kind == .Value && !fits_i64(constant.value)) {
|
||||||
|
return types.I64
|
||||||
|
}
|
||||||
|
if constant.kind == .Value {
|
||||||
|
return types.smallest_signed_for_literal(i64(constant.value))
|
||||||
|
}
|
||||||
|
return types.INVALID
|
||||||
case .Bool:
|
case .Bool:
|
||||||
return types.BOOL
|
return types.BOOL
|
||||||
case .Not:
|
case .Not:
|
||||||
@@ -1954,7 +2155,7 @@ infer_expr :: proc(
|
|||||||
_ = pop(&stack)
|
_ = pop(&stack)
|
||||||
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
||||||
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, .Cast,
|
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, .Cast,
|
||||||
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
.Comptime, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
||||||
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types)
|
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types)
|
||||||
_ = pop(&stack)
|
_ = pop(&stack)
|
||||||
case .Name:
|
case .Name:
|
||||||
@@ -2371,7 +2572,7 @@ infer_statements :: proc(
|
|||||||
open_float := false
|
open_float := false
|
||||||
const_val := i128(0)
|
const_val := i128(0)
|
||||||
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
|
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
|
||||||
constant := eval_constant(checker, statement.expr)
|
constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file)
|
||||||
if constant.kind == .Value && fits_i64(constant.value) {
|
if constant.kind == .Value && fits_i64(constant.value) {
|
||||||
open = true
|
open = true
|
||||||
const_val = constant.value
|
const_val = constant.value
|
||||||
@@ -2842,7 +3043,7 @@ expr_accepts_numeric_demand :: proc(
|
|||||||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if constant := eval_constant(checker, expr_id); constant.kind == .Value {
|
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); constant.kind == .Value {
|
||||||
return open_integer_accepts_demand(checker, constant.value, demand)
|
return open_integer_accepts_demand(checker, constant.value, demand)
|
||||||
}
|
}
|
||||||
if is_float_constant_expr(checker, expr_id) {
|
if is_float_constant_expr(checker, expr_id) {
|
||||||
@@ -2945,7 +3146,7 @@ infer_all :: proc(checker: ^Checker) {
|
|||||||
if global.external {
|
if global.external {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
constant := eval_constant(checker, global.expr)
|
constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file)
|
||||||
if constant.kind == .Value && fits_i64(constant.value) {
|
if constant.kind == .Value && fits_i64(constant.value) {
|
||||||
checker.global_open_const[index] = true
|
checker.global_open_const[index] = true
|
||||||
checker.global_const_value[index] = constant.value
|
checker.global_const_value[index] = constant.value
|
||||||
@@ -4120,6 +4321,21 @@ build_compound_expr :: proc(
|
|||||||
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
|
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
case .Comptime:
|
||||||
|
constant := Constant{kind=.Not_Constant}
|
||||||
|
if expr.left != ast.INVALID_EXPR {
|
||||||
|
constant = eval_integer_constant_in_context(checker, expr.left, pkg, file)
|
||||||
|
} else {
|
||||||
|
value, produced, ok := eval_comptime_statements(checker, expr.body, pkg, file, 0, nil, true)
|
||||||
|
if ok && produced {
|
||||||
|
constant = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero {
|
||||||
|
return build_constant_expr(checker, expr, constant, expected)
|
||||||
|
}
|
||||||
|
id := source.add(checker.diagnostics, expr.span, "expression cannot be evaluated at comptime")
|
||||||
|
return invalid_hir_expr(checker, expr.span, id)
|
||||||
case .Bool:
|
case .Bool:
|
||||||
return add_hir_expr(checker, hir.Expr{
|
return add_hir_expr(checker, hir.Expr{
|
||||||
kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer),
|
kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer),
|
||||||
@@ -4400,7 +4616,7 @@ build_expr :: proc(
|
|||||||
switch expr.kind {
|
switch expr.kind {
|
||||||
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
||||||
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed,
|
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed,
|
||||||
.Bool, .Cast, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
|
.Bool, .Cast, .Comptime, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
|
||||||
.Enum_Literal:
|
.Enum_Literal:
|
||||||
last = build_compound_expr(
|
last = build_compound_expr(
|
||||||
checker, expr, locals, global_reads, calls, frame.expected, pkg, file,
|
checker, expr, locals, global_reads, calls, frame.expected, pkg, file,
|
||||||
@@ -5092,7 +5308,7 @@ build_block :: proc(
|
|||||||
// `undefined`, open numeric constants, or arithmetic expressions.
|
// `undefined`, open numeric constants, or arithmetic expressions.
|
||||||
open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr)
|
open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr)
|
||||||
if open_const_decl {
|
if open_const_decl {
|
||||||
constant := eval_constant(checker, statement.expr)
|
constant := eval_integer_constant_in_context(checker, statement.expr, ctx.pkg, ctx.file)
|
||||||
open_const_decl = constant.kind == .Value && fits_i64(constant.value) ||
|
open_const_decl = constant.kind == .Value && fits_i64(constant.value) ||
|
||||||
is_float_constant_expr(checker, statement.expr)
|
is_float_constant_expr(checker, statement.expr)
|
||||||
}
|
}
|
||||||
@@ -7650,7 +7866,7 @@ build_globals :: proc(checker: ^Checker) {
|
|||||||
expected := types.INVALID
|
expected := types.INVALID
|
||||||
if is_runtime_type(checker, declared) {
|
if is_runtime_type(checker, declared) {
|
||||||
expected = declared
|
expected = declared
|
||||||
} else if constant := eval_constant(checker, global.expr);
|
} else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file);
|
||||||
constant.kind == .Value && fits_i64(constant.value) &&
|
constant.kind == .Value && fits_i64(constant.value) &&
|
||||||
is_runtime_type(checker, checker.global_types[global_index]) {
|
is_runtime_type(checker, checker.global_types[global_index]) {
|
||||||
// Open integer constant: build against its demanded/defaulted type. Gated
|
// Open integer constant: build against its demanded/defaulted type. Gated
|
||||||
|
|||||||
@@ -914,7 +914,7 @@ is_simple_range_bound :: proc(expr: ast.Expr) -> bool {
|
|||||||
|
|
||||||
prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) {
|
prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) {
|
||||||
#partial switch kind {
|
#partial switch kind {
|
||||||
case .Minus, .Ampersand, .Bang, .Keyword_Try:
|
case .Minus, .Ampersand, .Bang, .Dollar, .Keyword_Try:
|
||||||
return 20, true
|
return 20, true
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
@@ -931,6 +931,18 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
|
|||||||
left := ast.INVALID_EXPR
|
left := ast.INVALID_EXPR
|
||||||
if right_power, ok := prefix_binding_power(current(parser).kind); ok {
|
if right_power, ok := prefix_binding_power(current(parser).kind); ok {
|
||||||
operator := advance(parser)
|
operator := advance(parser)
|
||||||
|
if operator.kind == .Dollar && current(parser).kind == .Left_Brace {
|
||||||
|
body := parse_block(parser)
|
||||||
|
end := previous(parser)
|
||||||
|
left = add_expr(parser, ast.Expr{
|
||||||
|
kind = .Comptime,
|
||||||
|
span = span_from(operator.span, end.span),
|
||||||
|
body = body,
|
||||||
|
left = ast.INVALID_EXPR,
|
||||||
|
right = ast.INVALID_EXPR,
|
||||||
|
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
if parser.delimiter_depth > 0 {
|
if parser.delimiter_depth > 0 {
|
||||||
skip_newlines(parser)
|
skip_newlines(parser)
|
||||||
}
|
}
|
||||||
@@ -940,15 +952,17 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
|
|||||||
#partial switch operator.kind {
|
#partial switch operator.kind {
|
||||||
case .Ampersand: prefix_kind = .Address
|
case .Ampersand: prefix_kind = .Address
|
||||||
case .Bang: prefix_kind = .Not
|
case .Bang: prefix_kind = .Not
|
||||||
|
case .Dollar: prefix_kind = .Comptime
|
||||||
case .Keyword_Try: prefix_kind = .Try
|
case .Keyword_Try: prefix_kind = .Try
|
||||||
}
|
}
|
||||||
left = add_expr(parser, ast.Expr{
|
left = add_expr(parser, ast.Expr{
|
||||||
kind=prefix_kind,
|
kind = prefix_kind,
|
||||||
span=span_from(operator.span, operand_expr.span),
|
span = span_from(operator.span, operand_expr.span),
|
||||||
left=operand,
|
left = operand,
|
||||||
right=ast.INVALID_EXPR,
|
right = ast.INVALID_EXPR,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
left = parse_primary(parser, nesting)
|
left = parse_primary(parser, nesting)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,6 +567,42 @@ main func() void {}
|
|||||||
testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate)
|
testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
parser_accepts_comptime_prefix_and_block_expressions :: proc(t: ^testing.T) {
|
||||||
|
text := `sum func(a, b int) int { return a + b }
|
||||||
|
literal :: $32
|
||||||
|
call :: $sum(1, 2) + 3
|
||||||
|
block :: ${
|
||||||
|
yield 4
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&module)
|
||||||
|
|
||||||
|
literal := module.exprs[module.globals[0].expr]
|
||||||
|
call_add := module.exprs[module.globals[1].expr]
|
||||||
|
call := module.exprs[call_add.left]
|
||||||
|
block := module.exprs[module.globals[2].expr]
|
||||||
|
|
||||||
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
|
testing.expect_value(t, literal.kind, ast.Expr_Kind.Comptime)
|
||||||
|
testing.expect_value(t, module.exprs[literal.left].kind, ast.Expr_Kind.Integer)
|
||||||
|
testing.expect_value(t, call_add.kind, ast.Expr_Kind.Add)
|
||||||
|
testing.expect_value(t, call.kind, ast.Expr_Kind.Comptime)
|
||||||
|
testing.expect_value(t, module.exprs[call.left].kind, ast.Expr_Kind.Call)
|
||||||
|
testing.expect_value(t, block.kind, ast.Expr_Kind.Comptime)
|
||||||
|
testing.expect_value(t, len(block.body), 1)
|
||||||
|
testing.expect_value(t, module.statements[block.body[0]].kind, ast.Stmt_Kind.Yield)
|
||||||
|
}
|
||||||
|
|
||||||
nested_expression_source :: proc(call: bool, depth: int) -> string {
|
nested_expression_source :: proc(call: bool, depth: int) -> string {
|
||||||
builder := strings.builder_make()
|
builder := strings.builder_make()
|
||||||
defer strings.builder_destroy(&builder)
|
defer strings.builder_destroy(&builder)
|
||||||
@@ -2235,6 +2271,119 @@ main func() void {
|
|||||||
testing.expect(t, found_assign)
|
testing.expect(t, found_assign)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
comptime_expression_forces_integer_evaluation :: proc(t: ^testing.T) {
|
||||||
|
text := `sum func(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
max func(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
nested func(value int) int {
|
||||||
|
two :: 2
|
||||||
|
return sum(value, two)
|
||||||
|
}
|
||||||
|
make_array func($N usize) [N]u8 {
|
||||||
|
data [N]u8 = undefined
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
forced :: $sum(1, 2)
|
||||||
|
main func() i32 {
|
||||||
|
value i32 :: $sum(20, 22)
|
||||||
|
choice i32 :: $max(9, 3)
|
||||||
|
blocked i32 :: ${
|
||||||
|
local :: 5
|
||||||
|
yield sum(local, 6)
|
||||||
|
}
|
||||||
|
bytes [_]u8 :: make_array($nested(2))
|
||||||
|
if forced != 3 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if value != 42 {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if choice != 9 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if blocked != 11 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
if bytes.len != 4 {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
return 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(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
comptime_expression_diagnoses_unsupported_v1_evaluation :: proc(t: ^testing.T) {
|
||||||
|
text := `loop func() int {
|
||||||
|
while true {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
missing func() int {
|
||||||
|
if true {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recurse func(value int) int {
|
||||||
|
return recurse(value)
|
||||||
|
}
|
||||||
|
main func() void {
|
||||||
|
runtime i32 = 1
|
||||||
|
_ = $runtime
|
||||||
|
_ = $loop()
|
||||||
|
_ = $missing()
|
||||||
|
_ = $recurse(1)
|
||||||
|
_ = ${
|
||||||
|
value :: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
|
||||||
|
found := 0
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
if strings.contains(diagnostic.message, "expression cannot be evaluated at comptime") {
|
||||||
|
found += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testing.expect(t, found >= 5)
|
||||||
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
|
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
|
||||||
text := `broken func(value, value i8, nope void) void {}
|
text := `broken func(value, value i8, nope void) void {}
|
||||||
@@ -5615,6 +5764,16 @@ comptime_type_params_compile_and_run :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, state.exit_code, 0)
|
testing.expect_value(t, state.exit_code, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
comptime_eval_compile_and_run :: proc(t: ^testing.T) {
|
||||||
|
output := "/tmp/brolang-test-comptime-eval"
|
||||||
|
defer _ = os.remove(output)
|
||||||
|
status := compiler_core.compile_package("examples/programs/comptime_eval", output)
|
||||||
|
testing.expect_value(t, status, 0)
|
||||||
|
state := run_executable(output)
|
||||||
|
testing.expect_value(t, state.exit_code, 0)
|
||||||
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) {
|
lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) {
|
||||||
output := "/tmp/brolang-test-package-lazy-import"
|
output := "/tmp/brolang-test-package-lazy-import"
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
sum func(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
max func(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
nested func(value int) int {
|
||||||
|
two :: 2
|
||||||
|
return sum(value, two)
|
||||||
|
}
|
||||||
|
|
||||||
|
make_array func($N usize) [N]u8 {
|
||||||
|
data [N]u8 = undefined
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
forced :: $sum(1, 2)
|
||||||
|
|
||||||
|
main func() i32 {
|
||||||
|
value i32 :: $sum(20, 22)
|
||||||
|
choice i32 :: $max(9, 3)
|
||||||
|
blocked i32 :: ${
|
||||||
|
local :: 5
|
||||||
|
yield sum(local, 6)
|
||||||
|
}
|
||||||
|
bytes [_]u8 :: make_array($nested(2))
|
||||||
|
|
||||||
|
if forced != 3 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if value != 42 {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if choice != 9 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if blocked != 11 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
if bytes.len != 4 {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user