From adf142736cf95796a3d7863393e9106a3ced58d1 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 3 Jul 2026 15:54:28 +0200 Subject: [PATCH] more comptime eval --- LANGUAGE.md | 5 +- README.md | 3 +- TODO.md | 36 +- compiler/checker/checker.odin | 453 +----- compiler/checker/comptime.odin | 2061 ++++++++++++++++++++++++ compiler_tests.odin | 51 +- examples/programs/comptime_v1/main.bro | 138 ++ 7 files changed, 2266 insertions(+), 481 deletions(-) create mode 100644 compiler/checker/comptime.odin create mode 100644 examples/programs/comptime_v1/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index 063f0d2..46bec05 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -54,7 +54,8 @@ roadmap and milestone history. - 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 - 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 +- forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }` +- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, and `try`/`catch` - 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 - Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns @@ -82,7 +83,7 @@ roadmap and milestone history. ## PLANNED / DEFERRED -- broader Zig-style comptime execution +- comptime pointers, slices, aggregate comptime parameters, and calls through comptime-known function values/function pointers - tuples and native Brolang variadic functions - 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 diff --git a/README.md b/README.md index 5d132d5..42fd7a6 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,8 @@ Current prototype features: - Qualified imported globals and functions with package-aware symbol mangling - 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 -- Forced comptime expressions (`$sum(1, 2)`) and comptime value blocks (`${ yield 4 }`) for integer constant contexts +- Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`) +- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, and `try`/`catch` - Bodyless concrete C function declarations with exact external symbol names - Bodyless manual and imported C variadic declarations with default argument promotions - Ordered linking of additional C sources, objects, archives, and libraries diff --git a/TODO.md b/TODO.md index 52bc1f2..89b44a8 100644 --- a/TODO.md +++ b/TODO.md @@ -650,30 +650,38 @@ - v1 intentionally keeps `type` contextual to comptime parameter declarations; no inferred type parameters, first-class type values, or comptime execution -27.6 comptime-evaluable constants/functions +27.6 comptime-evaluable constants/functions (implemented) - `$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 + - v1 evaluator supported 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 + - broader typed 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 +27.7 broader Zig-style comptime execution (implemented; v1) + - `compiler/checker/comptime.odin` owns checker-local evaluator state, typed + comptime values, execution, and HIR materialization; `checker.odin` keeps type + checking, inference, specialization, and build orchestration + - typed `$` values cover bools, integers, floats, strings, arrays, structs, tagged + unions, enums, optionals, and fallibles + - supports mutable comptime locals/assignment, `if`, `while`, `for`, + `break`/`continue`, `defer`, `match`, value blocks/`yield`, direct calls to + bodyful Brolang functions, and `try`/`catch` + - successful `$` results materialize back into ordinary HIR expressions so lowering + and LLVM stay unchanged + - evaluation uses a fixed `100_000` step quota - 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 + - runtime-only behavior is rejected in comptime: external/bodyless `c_func`, + writable globals, pointers/slices, address/deref storage APIs, pointer captures, + and function-pointer calls + - v1 keeps integer-only `$N` specialization keys; aggregate comptime parameters, + stable aggregate serialization, comptime pointers/slices, and calls through + comptime-known function values/function pointers are deferred 28. brolang build system (requires comptime execution) @@ -1270,6 +1278,8 @@ max func($T type, a, b T) T { ... } # implemented: comptime type params x :: $32 # implemented: force comptime expression evaluation n :: $sum(1, 2) # implemented: ordinary functions can run at comptime res :: ${ yield 4 } # implemented: comptime value block +p :: $Point { x = 1, y = 2 } # implemented: typed aggregate comptime values +total :: $sum_loop(4) # implemented: mutable locals/loops/defer/match/try/catch ``` ## A word on memory allocation diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index c981c46..baac6c5 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -6,7 +6,6 @@ import "../source" import "../symbol" import "../target" import "../types" -import "base:intrinsics" import "core:fmt" import "core:mem" import "core:slice" @@ -33,18 +32,6 @@ Spec :: struct { hir_id: hir.Function_Id, } -Comptime_Value_Kind :: enum u8 { - Integer, - Type, -} - -Comptime_Value :: struct { - name: symbol.Id, - type: types.Type, - value: i128, - kind: Comptime_Value_Kind, -} - Infer_Local :: struct { name: symbol.Id, type: types.Type, @@ -118,19 +105,6 @@ Build_Ctx :: struct { loop_floor: int, } -Constant_Kind :: enum { - Unknown, - Not_Constant, - Value, - Overflow, - Div_By_Zero, -} - -Constant :: struct { - kind: Constant_Kind, - value: i128, -} - Function_Index_Entry :: struct { scope: ast.Package_Id, name: symbol.Id, @@ -202,26 +176,6 @@ type_label :: proc(checker: ^Checker, value: types.Type) -> string { return types.name(value) } -find_comptime_value :: proc(values: []Comptime_Value, name: symbol.Id) -> (Comptime_Value, bool) { - for index := len(values) - 1; index >= 0; index -= 1 { - if values[index].name == name { - return values[index], true - } - } - return {}, false -} - -current_comptime_value :: proc(checker: ^Checker, name: symbol.Id) -> (Comptime_Value, bool) { - return find_comptime_value(checker.current_comptime_values, name) -} - -current_comptime_type :: proc(checker: ^Checker, name: symbol.Id) -> (types.Type, bool) { - if value, ok := current_comptime_value(checker, name); ok && value.kind == .Type { - return value.type, true - } - return types.INVALID, false -} - is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool { item, ok := types.node(&checker.module.types, value) return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0 @@ -260,379 +214,6 @@ comptime_param_count :: proc(function: ast.Function) -> int { return count } -comptime_values_equal :: proc(left, right: []Comptime_Value) -> bool { - if len(left) != len(right) { - return false - } - for value, index in left { - other := right[index] - if value.name != other.name || value.kind != other.kind || !types.equal(value.type, other.type) || - (value.kind == .Integer && value.value != other.value) { - return false - } - } - return true -} - -Constant_Frame :: struct { - expr: ast.Expr_Id, - stage: u8, -} - -eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { - if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { - return Constant{kind = .Not_Constant} - } - stack := checker.constant_stack - clear_dynamic_array(&stack) - defer { - clear_dynamic_array(&stack) - checker.constant_stack = stack - } - append(&stack, Constant_Frame{expr=expr_id}) - - for len(stack) > 0 { - frame_index := len(stack)-1 - frame := stack[frame_index] - if checker.constants[frame.expr].kind != .Unknown { - _ = pop(&stack) - continue - } - expr := checker.ast_module.exprs[frame.expr] - if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && - expr.kind != .Div && expr.kind != .Negate { - result := Constant{kind = .Not_Constant} - if expr.kind == .Integer { - result = Constant{kind = .Value, value = i128(expr.integer)} - } - checker.constants[frame.expr] = result - _ = pop(&stack) - continue - } - if frame.stage == 0 { - stack[frame_index].stage = 1 - if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.ast_module.exprs) && - checker.constants[expr.left].kind == .Unknown { - append(&stack, Constant_Frame{expr=expr.left}) - } - continue - } - if frame.stage == 1 && expr.kind == .Negate { - operand := Constant{kind = .Not_Constant} - if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { - 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 { - result = Constant{kind = .Overflow} - } else if operand.kind == .Value { - value, overflow := intrinsics.overflow_sub(i128(0), operand.value) - result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} - } - checker.constants[frame.expr] = result - _ = pop(&stack) - continue - } - if frame.stage == 1 { - stack[frame_index].stage = 2 - if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.ast_module.exprs) && - checker.constants[expr.right].kind == .Unknown { - append(&stack, Constant_Frame{expr=expr.right}) - } - continue - } - left := Constant{kind = .Not_Constant} - right := Constant{kind = .Not_Constant} - if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { - left = checker.constants[expr.left] - } - if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.constants) { - 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 { - 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} - } - } - checker.constants[frame.expr] = result - _ = pop(&stack) - } - return checker.constants[expr_id] -} - -eval_integer_constant_in_context :: proc( - checker: ^Checker, - expr_id: ast.Expr_Id, - pkg: ast.Package_Id, - file: ast.File_Id, - depth := 0, - values: []Comptime_Value = nil, -) -> Constant { - if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { - return Constant{kind = .Not_Constant} - } - expr := checker.ast_module.exprs[expr_id] - #partial switch expr.kind { - case .Integer: - return Constant{kind = .Value, value = i128(expr.integer)} - case .Bool: - return Constant{kind = .Value, value = i128(expr.integer)} - case .Name: - 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.kind == .Integer { - return Constant{kind = .Value, value = value.value} - } - } - } - target_pkg, available := expr_package(checker, expr, pkg, file, false) - if !available { - return Constant{kind = .Not_Constant} - } - global := find_global(checker, expr.name, target_pkg) - if global == ast.INVALID_GLOBAL || int(global) >= len(checker.ast_module.globals) { - return Constant{kind = .Not_Constant} - } - g := checker.ast_module.globals[global] - if g.external || !g.immutable { - return Constant{kind = .Not_Constant} - } - 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: - operand := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values) - if operand.kind == .Value { - value, overflow := intrinsics.overflow_sub(i128(0), operand.value) - return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} - } - 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: - 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 == .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} - } - if left.kind != .Value || right.kind != .Value { - return Constant{kind = .Not_Constant} - } - value: i128 - overflow: 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 { - return Constant{kind = .Div_By_Zero} - } - value = left.value / right.value - case: - value, overflow = intrinsics.overflow_add(left.value, right.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} - } - 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 { if !types.is_signed(value_type, selected) { return false @@ -1927,24 +1508,7 @@ infer_compound_expr :: proc( store := &checker.module.types #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 + return infer_comptime_expr_type(checker, expr, pkg, file) case .Bool: return types.BOOL case .Not: @@ -4322,20 +3886,7 @@ build_compound_expr :: proc( 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) + return build_comptime_expr(checker, expr, expected, pkg, file) case .Bool: return add_hir_expr(checker, hir.Expr{ kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer), diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin new file mode 100644 index 0000000..c4b3e48 --- /dev/null +++ b/compiler/checker/comptime.odin @@ -0,0 +1,2061 @@ +package checker + +import "../ast" +import "../hir" +import "../source" +import "../symbol" +import "../types" +import "base:intrinsics" +import "core:mem" + +COMPTIME_EVAL_QUOTA :: 100_000 + +Comptime_Value_Kind :: enum u8 { + Integer, + Type, +} + +Comptime_Value :: struct { + name: symbol.Id, + type: types.Type, + value: i128, + kind: Comptime_Value_Kind, +} + +Constant_Kind :: enum { + Unknown, + Not_Constant, + Value, + Overflow, + Div_By_Zero, +} + +Constant :: struct { + kind: Constant_Kind, + value: i128, +} + +Constant_Frame :: struct { + expr: ast.Expr_Id, + stage: u8, +} + +find_comptime_value :: proc(values: []Comptime_Value, name: symbol.Id) -> (Comptime_Value, bool) { + for index := len(values) - 1; index >= 0; index -= 1 { + if values[index].name == name { + return values[index], true + } + } + return {}, false +} + +current_comptime_value :: proc(checker: ^Checker, name: symbol.Id) -> (Comptime_Value, bool) { + return find_comptime_value(checker.current_comptime_values, name) +} + +current_comptime_type :: proc(checker: ^Checker, name: symbol.Id) -> (types.Type, bool) { + if value, ok := current_comptime_value(checker, name); ok && value.kind == .Type { + return value.type, true + } + return types.INVALID, false +} + +comptime_values_equal :: proc(left, right: []Comptime_Value) -> bool { + if len(left) != len(right) { + return false + } + for value, index in left { + other := right[index] + if value.name != other.name || value.kind != other.kind || !types.equal(value.type, other.type) || + (value.kind == .Integer && value.value != other.value) { + return false + } + } + return true +} + +eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return Constant{kind = .Not_Constant} + } + stack := checker.constant_stack + clear_dynamic_array(&stack) + defer { + clear_dynamic_array(&stack) + checker.constant_stack = stack + } + append(&stack, Constant_Frame{expr=expr_id}) + + for len(stack) > 0 { + frame_index := len(stack)-1 + frame := stack[frame_index] + if checker.constants[frame.expr].kind != .Unknown { + _ = pop(&stack) + continue + } + expr := checker.ast_module.exprs[frame.expr] + if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && + expr.kind != .Div && expr.kind != .Negate { + result := Constant{kind = .Not_Constant} + if expr.kind == .Integer { + result = Constant{kind = .Value, value = i128(expr.integer)} + } + checker.constants[frame.expr] = result + _ = pop(&stack) + continue + } + if frame.stage == 0 { + stack[frame_index].stage = 1 + if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.ast_module.exprs) && + checker.constants[expr.left].kind == .Unknown { + append(&stack, Constant_Frame{expr=expr.left}) + } + continue + } + if frame.stage == 1 && expr.kind == .Negate { + operand := Constant{kind = .Not_Constant} + if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { + 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 { + result = Constant{kind = .Overflow} + } else if operand.kind == .Value { + value, overflow := intrinsics.overflow_sub(i128(0), operand.value) + result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} + } + checker.constants[frame.expr] = result + _ = pop(&stack) + continue + } + if frame.stage == 1 { + stack[frame_index].stage = 2 + if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.ast_module.exprs) && + checker.constants[expr.right].kind == .Unknown { + append(&stack, Constant_Frame{expr=expr.right}) + } + continue + } + left := Constant{kind = .Not_Constant} + right := Constant{kind = .Not_Constant} + if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { + left = checker.constants[expr.left] + } + if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.constants) { + 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 { + 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} + } + } + checker.constants[frame.expr] = result + _ = pop(&stack) + } + return checker.constants[expr_id] +} + +Ct_Value_Id :: distinct u32 +INVALID_CT_VALUE :: Ct_Value_Id(0xffff_ffff) + +ct_value_id :: proc(index: int) -> Ct_Value_Id { + assert(index >= 0 && u64(index) < u64(INVALID_CT_VALUE)) + return Ct_Value_Id(index) +} + +Ct_Value_Kind :: enum u8 { + Invalid, + Void, + Integer, + Float, + Bool, + String, + Range, + Array, + Struct, + None, + Optional_Some, + Fallible, +} + +Ct_Error_Kind :: enum u8 { + None, + Not_Comptime, + Overflow, + Div_By_Zero, + Quota, +} + +Ct_Value :: struct { + kind: Ct_Value_Kind, + type: types.Type, + integer: i128, + float: f64, + index: u64, + start: u32, + count: u32, + active: i64, +} + +Ct_Binding :: struct { + name: symbol.Id, + type: types.Type, + value: Ct_Value_Id, + mutable: bool, +} + +Ct_Flow_Kind :: enum u8 { + Normal, + Return, + Yield, + Break, + Continue, +} + +Ct_Flow :: struct { + kind: Ct_Flow_Kind, + value: Ct_Value_Id, + label: symbol.Id, +} + +Ct_State :: struct { + checker: ^Checker, + pkg: ast.Package_Id, + file: ast.File_Id, + result: types.Type, + values: [dynamic]Ct_Value, + children: [dynamic]Ct_Value_Id, + bindings: [dynamic]Ct_Binding, + defers: [dynamic]ast.Stmt_Id, + steps: int, + error: Ct_Error_Kind, + diagnostic: source.Diagnostic_Id, + silent: bool, +} + +ct_state_make :: proc( + checker: ^Checker, + pkg: ast.Package_Id, + file: ast.File_Id, + result := types.INVALID, + values: []Comptime_Value = nil, + diagnose := true, +) -> Ct_State { + state: Ct_State + state.checker = checker + state.pkg = pkg + state.file = file + state.result = result + state.error = .None + state.diagnostic = source.INVALID_DIAGNOSTIC + state.silent = !diagnose + state.values.allocator = checker.allocator + state.children.allocator = checker.allocator + state.bindings.allocator = checker.allocator + state.defers.allocator = checker.allocator + for value in values { + if value.kind == .Integer { + id := ct_add_value(&state, Ct_Value{kind=.Integer, type=value.type, integer=value.value}) + append(&state.bindings, Ct_Binding{name=value.name, type=value.type, value=id, mutable=false}) + } + } + return state +} + +ct_state_destroy :: proc(state: ^Ct_State) { + delete(state.values) + delete(state.children) + delete(state.bindings) + delete(state.defers) +} + +ct_add_value :: proc(state: ^Ct_State, value: Ct_Value) -> Ct_Value_Id { + id := ct_value_id(len(state.values)) + append(&state.values, value) + return id +} + +ct_child_slice :: proc(state: ^Ct_State, value: Ct_Value) -> []Ct_Value_Id { + start := int(value.start) + end := start+int(value.count) + if start < 0 || end > len(state.children) { + return nil + } + return state.children[start:end] +} + +ct_fail :: proc(state: ^Ct_State, kind: Ct_Error_Kind, span: source.Span, message: string) -> bool { + if state.error == .None { + state.error = kind + if !state.silent && len(message) > 0 { + state.diagnostic = source.add(state.checker.diagnostics, span, message) + } + } + return false +} + +ct_failf :: proc(state: ^Ct_State, kind: Ct_Error_Kind, span: source.Span, fmt: string, args: ..any) -> bool { + if state.error == .None { + state.error = kind + if !state.silent { + state.diagnostic = source.addf(state.checker.diagnostics, span, fmt, ..args) + } + } + return false +} + +ct_step :: proc(state: ^Ct_State, span: source.Span) -> bool { + state.steps += 1 + if state.steps > COMPTIME_EVAL_QUOTA { + return ct_fail(state, .Quota, span, "comptime evaluation exceeded the step quota") + } + return true +} + +ct_find_binding_index :: proc(state: ^Ct_State, name: symbol.Id) -> (int, bool) { + for index := len(state.bindings) - 1; index >= 0; index -= 1 { + if state.bindings[index].name == name { + return index, true + } + } + return -1, false +} + +ct_flow :: proc(kind: Ct_Flow_Kind, value := INVALID_CT_VALUE, label := symbol.INVALID) -> Ct_Flow { + return Ct_Flow{kind=kind, value=value, label=label} +} + +ct_bool_value :: proc(state: ^Ct_State, id: Ct_Value_Id) -> (bool, bool) { + if id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return false, false + } + value := state.values[id] + if value.kind == .Bool { + return value.integer != 0, true + } + return false, false +} + +ct_integer_value :: proc(state: ^Ct_State, id: Ct_Value_Id) -> (i128, bool) { + if id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return 0, false + } + value := state.values[id] + if value.kind == .Integer || value.kind == .Bool { + return value.integer, true + } + return 0, false +} + +ct_is_integer_like :: proc(value: Ct_Value) -> bool { + return value.kind == .Integer || value.kind == .Bool +} + +ct_default_integer_type :: proc(value: i128) -> types.Type { + if fits_i64(value) { + return types.smallest_signed_for_literal(i64(value)) + } + return types.I64 +} + +ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type, span: source.Span) -> (Ct_Value_Id, bool) { + if id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return INVALID_CT_VALUE, false + } + if !types.is_valid(expected) || types.is_constraint(expected) { + return id, true + } + value := state.values[id] + if types.equal(value.type, expected) { + return id, true + } + store := &state.checker.module.types + if value.kind == .Array { + expected_item, expected_ok := types.node(store, expected) + value_item, value_ok := types.node(store, value.type) + if expected_ok && value_ok && expected_item.kind == .Array && value_item.kind == .Array && + expected_item.inferred_count && types.equal(expected_item.child, value_item.child) { + value.type = types.with_array_count(store, expected, value_item.count) + return ct_add_value(state, value), true + } + } + if value.kind == .None { + if types.is_optional(expected, store) { + value.type = expected + return ct_add_value(state, value), true + } + return INVALID_CT_VALUE, ct_fail(state, .Not_Comptime, span, "'none' requires an optional context") + } + if types.is_optional(expected, store) { + child := types.child_type(expected, store) + coerced, ok := ct_coerce_value(state, id, child, span) + if ok { + start := u32(len(state.children)) + append(&state.children, coerced) + return ct_add_value(state, Ct_Value{kind=.Optional_Some, type=expected, start=start, count=1}), true + } + return INVALID_CT_VALUE, false + } + if ct_is_integer_like(value) { + if types.is_concrete_integer(expected) || types.is_enum(expected, store) { + backing := expected + if item, ok := types.node(store, expected); ok && item.kind == .Enum { + backing = item.child + } + if !fits_integer_type(value.integer, backing, state.checker.target) { + return INVALID_CT_VALUE, ct_failf( + state, .Not_Comptime, span, "integer constant %d does not fit in %s", + value.integer, types.name(expected), + ) + } + value.type = expected + value.kind = .Integer + return ct_add_value(state, value), true + } + if types.is_float(expected, state.checker.target) { + return ct_add_value(state, Ct_Value{kind=.Float, type=expected, float=f64(value.integer)}), true + } + } + if value.kind == .Float && types.is_float(expected, state.checker.target) { + value.type = expected + return ct_add_value(state, value), true + } + return INVALID_CT_VALUE, ct_failf( + state, .Not_Comptime, span, "cannot implicitly convert %s to %s at comptime", + types.name(value.type), types.name(expected), + ) +} + +ct_materialize_value :: proc( + state: ^Ct_State, + id: Ct_Value_Id, + span: source.Span, + expected := types.INVALID, +) -> hir.Expr_Id { + checker := state.checker + if id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC) + } + materialized := id + if types.is_valid(expected) { + if coerced, ok := ct_coerce_value(state, id, expected, span); ok { + materialized = coerced + } else { + return invalid_hir_expr(checker, span, state.diagnostic, expected) + } + } + value := state.values[materialized] + #partial switch value.kind { + case .Integer: + if types.is_enum(value.type, &checker.module.types) { + int_value := i64(value.integer) if value.integer < 0 else transmute(i64)u64(value.integer) + return add_hir_expr(checker, hir.Expr{ + kind=.Integer, span=span, type=value.type, integer=int_value, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + return build_constant_expr(checker, ast.Expr{span=span}, Constant{kind=.Value, value=value.integer}, value.type) + case .Bool: + return add_hir_expr(checker, hir.Expr{ + kind=.Bool, span=span, type=types.BOOL, integer=i64(value.integer), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Float: + bits := transmute(i64)value.float + if types.bits(value.type, checker.target) == 32 { + bits = i64(transmute(u32)f32(value.float)) + } + return add_hir_expr(checker, hir.Expr{ + kind=.Float, span=span, type=value.type, integer=bits, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .String: + return add_hir_expr(checker, hir.Expr{ + kind=.String, span=span, type=value.type, integer=i64(value.index), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Range: + children := ct_child_slice(state, value) + args := make([]hir.Expr_Id, 2, checker.allocator) + for child, index in children[:min(2, len(children))] { + args[index] = ct_materialize_value(state, child, span) + } + return add_hir_expr(checker, hir.Expr{ + kind=.Range, span=span, type=value.type, integer=value.active, args=args, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Array, .Struct: + children := ct_child_slice(state, value) + args := make([]hir.Expr_Id, len(children), checker.allocator) + if value.kind == .Array { + item, _ := types.node(&checker.module.types, value.type) + for child, index in children { + args[index] = ct_materialize_value(state, child, span, item.child) + } + } else if types.is_union(value.type, &checker.module.types) { + fields := types.fields_for(&checker.module.types, value.type) + payload_type := types.INVALID + if value.active >= 0 && value.active < i64(len(fields)) { + payload_type = fields[value.active].type + } + for child, index in children { + args[index] = hir.INVALID_EXPR + if child != INVALID_CT_VALUE && types.is_valid(payload_type) && !types.is_void(payload_type) { + args[index] = ct_materialize_value(state, child, span, payload_type) + } + } + } else { + fields := types.fields_for(&checker.module.types, value.type) + for child, index in children { + field_type := fields[index].type if index < len(fields) else types.INVALID + args[index] = ct_materialize_value(state, child, span, field_type) + } + } + kind := hir.Expr_Kind.Array if value.kind == .Array else hir.Expr_Kind.Struct + return add_hir_expr(checker, hir.Expr{ + kind=kind, span=span, type=value.type, args=args, integer=value.active, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .None: + return add_hir_expr(checker, hir.Expr{ + kind=.None, span=span, type=value.type, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Optional_Some: + children := ct_child_slice(state, value) + child := hir.INVALID_EXPR + if len(children) > 0 { + child_type := types.child_type(value.type, &checker.module.types) + child = ct_materialize_value(state, children[0], span, child_type) + } + return add_hir_expr(checker, hir.Expr{ + kind=.Optional_Some, span=span, type=value.type, left=child, + target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Fallible: + children := ct_child_slice(state, value) + args := make([]hir.Expr_Id, 1, checker.allocator) + args[0] = hir.INVALID_EXPR + if len(children) > 0 && children[0] != INVALID_CT_VALUE { + fallible_item, _ := types.node(&checker.module.types, value.type) + payload_type := fallible_item.extra if value.active != 0 else fallible_item.child + if !types.is_void(payload_type) { + args[0] = ct_materialize_value(state, children[0], span, payload_type) + } + } + return add_hir_expr(checker, hir.Expr{ + kind=.Struct, span=span, type=value.type, args=args, integer=value.active, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC, value.type) +} + +ct_eval_expr :: proc( + state: ^Ct_State, + expr_id: ast.Expr_Id, + expected := types.INVALID, + depth := 0, +) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + if depth > 128 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, source.Span{}, "expression cannot be evaluated at comptime") + } + expr := checker.ast_module.exprs[expr_id] + if !ct_step(state, expr.span) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + store := &checker.module.types + #partial switch expr.kind { + case .Integer: + value_type := expected if types.is_concrete_integer(expected) || types.is_enum(expected, store) else ct_default_integer_type(i128(expr.integer)) + id := ct_add_value(state, Ct_Value{kind=.Integer, type=value_type, integer=i128(expr.integer)}) + if types.is_valid(expected) { + return ct_coerce_expr_value(state, id, expected, expr.span) + } + return id, ct_flow(.Normal), true + case .Bool: + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=i128(expr.integer)}), ct_flow(.Normal), true + case .Float: + value_type := expected if types.is_float(expected, checker.target) else types.F64 + return ct_add_value(state, Ct_Value{kind=.Float, type=value_type, float=transmute(f64)expr.integer}), ct_flow(.Normal), true + case .String: + return ct_add_value(state, Ct_Value{kind=.String, type=string_literal_type(checker, expr.integer), index=expr.integer}), ct_flow(.Normal), true + case .Name: + if !symbol.is_valid(expr.qualifier) { + if index, ok := ct_find_binding_index(state, expr.name); ok { + return state.bindings[index].value, ct_flow(.Normal), true + } + if value, ok := current_comptime_value(checker, expr.name); ok { + if value.kind == .Integer { + id := ct_add_value(state, Ct_Value{kind=.Integer, type=value.type, integer=value.value}) + return id, ct_flow(.Normal), true + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "type parameter '%s' is not a runtime value", symbol_text(checker, expr.name)) + } + } + if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, state.pkg, state.file); enum_ok { + member, ok := find_enum_member(checker, enum_type, expr.name) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown enum member '%s'", symbol_text(checker, expr.name)) + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=enum_type, integer=member.value}), ct_flow(.Normal), true + } + 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 imported package") + } + global := find_global(checker, expr.name, target_pkg) + if global == ast.INVALID_GLOBAL || int(global) >= len(checker.ast_module.globals) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unresolved comptime value '%s'", symbol_text(checker, expr.name)) + } + g := checker.ast_module.globals[global] + if g.external || !g.immutable || g.writable { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "global '%s' is not comptime-known", symbol_text(checker, expr.name)) + } + global_expected := type_from_syntax(checker, g.type, g.pkg, g.file) + return ct_eval_expr(state, g.expr, global_expected, depth+1) + case .Comptime: + if expr.left != ast.INVALID_EXPR { + return ct_eval_expr(state, expr.left, expected, depth+1) + } + flow, ok := ct_exec_statements(state, expr.body, true, depth+1) + if ok && flow.kind == .Yield { + return flow.value, ct_flow(.Normal), true + } + return INVALID_CT_VALUE, flow, ct_fail(state, .Not_Comptime, expr.span, "comptime block must yield a value") + case .Array: + return ct_eval_array_expr(state, expr, expected, depth+1) + case .Struct_Literal: + return ct_eval_struct_expr(state, expr, expected, depth+1) + case .Enum_Literal: + return ct_eval_enum_literal(state, expr, expected, depth+1) + case .None: + if !types.is_optional(expected, store) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "'none' requires an optional context") + } + return ct_add_value(state, Ct_Value{kind=.None, type=expected}), ct_flow(.Normal), true + case .Field: + if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, state.pkg, state.file); enum_ok { + member, ok := find_enum_member(checker, enum_type, expr.name) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown enum member '%s'", symbol_text(checker, expr.name)) + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=enum_type, integer=member.value}), ct_flow(.Normal), true + } + base_id, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + return ct_eval_field_value(state, base_id, expr.name, expr.span) + case .Index: + base_id, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + index_id, index_flow, index_ok := ct_eval_expr(state, expr.right, types.USIZE, depth+1) + if !index_ok || index_flow.kind != .Normal { + return INVALID_CT_VALUE, index_flow, index_ok + } + index_value, index_is_int := ct_integer_value(state, index_id) + if !index_is_int || index_value < 0 || index_value > i128(0x7fff_ffff) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime index must be a non-negative integer") + } + return ct_eval_index_value(state, base_id, int(index_value), expr.span) + case .Unwrap: + value, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + return ct_unwrap_optional(state, value, expr.span) + case .Orelse: + value, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + v := state.values[value] + if v.kind == .Optional_Some { + children := ct_child_slice(state, v) + if len(children) > 0 { + return children[0], ct_flow(.Normal), true + } + } + if v.kind == .None { + child := types.child_type(v.type, store) + return ct_eval_expr(state, expr.right, child, depth+1) + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "'orelse' requires an optional left operand") + case .Try: + return ct_eval_try_expr(state, expr, depth+1) + case .Catch: + return ct_eval_catch_expr(state, expr, expected, depth+1) + case .Range: + left, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + right, right_flow, right_ok := ct_eval_expr(state, expr.right, state.values[left].type, depth+1) + if !right_ok || right_flow.kind != .Normal { + return INVALID_CT_VALUE, right_flow, right_ok + } + child_type := types.widest(state.values[left].type, state.values[right].type) + if !types.is_concrete_integer(child_type) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "range bounds must be compatible concrete integers") + } + left, _ = ct_coerce_value(state, left, child_type, expr.span) + right, _ = ct_coerce_value(state, right, child_type, expr.span) + start := u32(len(state.children)) + append(&state.children, left, right) + return ct_add_value(state, Ct_Value{ + kind=.Range, type=types.range(store, child_type), start=start, count=2, active=i64(expr.integer), + }), ct_flow(.Normal), true + case .Negate, .Not: + value, flow, ok := ct_eval_expr(state, expr.left, expected, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + 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) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + right, right_flow, right_ok := ct_eval_expr(state, expr.right, state.values[left].type, depth+1) + if !right_ok || right_flow.kind != .Normal { + return INVALID_CT_VALUE, right_flow, right_ok + } + return ct_eval_binary(state, expr.kind, left, right, expr.span) + case .And, .Or: + left, flow, ok := ct_eval_expr(state, expr.left, types.BOOL, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + left_bool, bool_ok := ct_bool_value(state, left) + if !bool_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "'and'/'or' require bool operands") + } + if expr.kind == .And && !left_bool { + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=0}), ct_flow(.Normal), true + } + if expr.kind == .Or && left_bool { + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1}), ct_flow(.Normal), true + } + return ct_eval_expr(state, expr.right, types.BOOL, depth+1) + case .Call: + return ct_eval_call_expr(state, expr, expected, depth+1) + case .Cast: + target := type_from_syntax(checker, expr.type, state.pkg, state.file) + value, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + return ct_scalar_cast(state, value, target, expr.span) + case .Address, .Deref, .Slice: + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "pointers and slices are not supported in comptime evaluation yet") + case .Type, .Undefined, .Keyed: + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "expression cannot be evaluated at comptime") + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "expression cannot be evaluated at comptime") +} + +ct_coerce_expr_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { + coerced, ok := ct_coerce_value(state, id, expected, span) + return coerced, ct_flow(.Normal), ok +} + +ct_eval_array_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + store := &checker.module.types + element_type := types.INVALID + result_type := expected + expected_node, has_expected := types.node(store, expected) + if has_expected && expected_node.kind == .Array { + element_type = expected_node.child + if !expected_node.inferred_count && expected_node.count != u64(len(expr.args)) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( + state, .Not_Comptime, expr.span, "array literal expects %d elements, got %d", + expected_node.count, len(expr.args), + ) + } + if expected_node.inferred_count { + result_type = types.with_array_count(store, expected, u64(len(expr.args))) + } + } else { + has_expected = false + result_type = types.INVALID + } + start := u32(len(state.children)) + for arg in expr.args { + value, flow, ok := ct_eval_expr(state, arg, element_type, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + if !types.is_valid(element_type) { + element_type = state.values[value].type + } else if !types.equal(element_type, state.values[value].type) { + element_type = types.widest(element_type, state.values[value].type) + } + append(&state.children, value) + } + if !types.is_valid(element_type) { + element_type = types.I64 + } + if !has_expected { + result_type = types.array(store, element_type, u64(len(expr.args)), false) + } + children := state.children[int(start):int(start)+len(expr.args)] + for &child in children { + coerced, ok := ct_coerce_value(state, child, element_type, expr.span) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + child = coerced + } + return ct_add_value(state, Ct_Value{kind=.Array, type=result_type, start=start, count=u32(len(expr.args))}), ct_flow(.Normal), true +} + +ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + store := &checker.module.types + struct_type := types.INVALID + if symbol.is_valid(expr.name) { + target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false) + struct_type = types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID + struct_type = types.resolve_alias(struct_type, store) + } else { + struct_type = types.resolve_alias(expected, store) + } + if !types.is_record(struct_type, store) || types.is_opaque_struct(struct_type, store) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown or opaque record type '%s'", symbol_text(checker, expr.name)) + } + fields := types.fields_for(store, struct_type) + union_record := types.is_union(struct_type, store) + if union_record && len(expr.args) != 1 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "union literal requires exactly one field initializer") + } + values := make([]Ct_Value_Id, 1 if union_record else len(fields), checker.allocator) + initialized := make([]bool, len(fields), checker.allocator) + defer { + delete(values, checker.allocator) + delete(initialized, checker.allocator) + } + for &value in values { + value = INVALID_CT_VALUE + } + active_field: i64 + for keyed in expr.args { + keyed_expr := checker.ast_module.exprs[keyed] + index, field, field_ok := find_struct_field(checker, struct_type, keyed_expr.name) + if !field_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, keyed_expr.span, "unknown struct field '%s'", symbol_text(checker, keyed_expr.name)) + } + if initialized[index] { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, keyed_expr.span, "duplicate initializer for struct field '%s'", symbol_text(checker, keyed_expr.name)) + } + initialized[index] = true + value_index := 0 if union_record else index + active_field = i64(index) + if keyed_expr.left == ast.INVALID_EXPR { + if !types.is_void(field.type) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, keyed_expr.span, "field '%s' requires a value", symbol_text(checker, keyed_expr.name)) + } + continue + } + value, flow, value_ok := ct_eval_expr(state, keyed_expr.left, field.type, depth+1) + if !value_ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, value_ok + } + value, value_ok = ct_coerce_value(state, value, field.type, keyed_expr.span) + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + values[value_index] = value + } + if !union_record { + for field, index in fields { + if values[index] == INVALID_CT_VALUE { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name))) + } + } + } + start := u32(len(state.children)) + append(&state.children, ..values) + return ct_add_value(state, Ct_Value{kind=.Struct, type=struct_type, start=start, count=u32(len(values)), active=active_field}), ct_flow(.Normal), true +} + +ct_eval_enum_literal :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + store := &checker.module.types + if types.is_tagged_union(expected, store) { + index, field, found := find_struct_field(checker, expected, expr.name) + if !found { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) + } + payload := INVALID_CT_VALUE + if expr.left != ast.INVALID_EXPR { + value, flow, ok := ct_eval_expr(state, expr.left, field.type, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + payload, ok = ct_coerce_value(state, value, field.type, expr.span) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + } else if !types.is_void(field.type) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "variant '.%s' on '%s' needs a payload", symbol_text(checker, expr.name), type_label(checker, expected)) + } + start := u32(len(state.children)) + append(&state.children, payload) + return ct_add_value(state, Ct_Value{kind=.Struct, type=expected, start=start, count=1, active=i64(index)}), ct_flow(.Normal), true + } + if expr.left != ast.INVALID_EXPR { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "'.%s{...}' requires a tagged-union context", symbol_text(checker, expr.name)) + } + if !types.is_enum(expected, store) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "'.%s' requires an enum context", symbol_text(checker, expr.name)) + } + member, ok := find_enum_member(checker, expected, expr.name) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown enum member '%s'", symbol_text(checker, expr.name)) + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=expected, integer=member.value}), ct_flow(.Normal), true +} + +ct_eval_field_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, name: symbol.Id, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + if base_id == INVALID_CT_VALUE || int(base_id) >= len(state.values) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + base := state.values[base_id] + field_name := symbol_text(checker, name) + if base.kind == .Array && field_name == "len" { + return ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(base.count)}), ct_flow(.Normal), true + } + if base.kind == .String && field_name == "len" { + length := i128(0) + if base.index < u64(len(checker.ast_module.strings)) { + length = i128(len(checker.ast_module.strings[base.index])) + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=length}), ct_flow(.Normal), true + } + if base.kind != .Struct { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "unknown comptime field '%s'", field_name) + } + index, field, ok := find_struct_field(checker, base.type, name) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "unknown struct field '%s'", field_name) + } + children := ct_child_slice(state, base) + if types.is_union(base.type, &checker.module.types) { + if int(base.active) != index || len(children) == 0 || children[0] == INVALID_CT_VALUE { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "variant '%s' has no payload to read", field_name) + } + return children[0], ct_flow(.Normal), true + } + if index < 0 || index >= len(children) || types.is_void(field.type) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "field '%s' has no value", field_name) + } + return children[index], ct_flow(.Normal), true +} + +ct_eval_index_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, index: int, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + if base_id == INVALID_CT_VALUE || int(base_id) >= len(state.values) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + base := state.values[base_id] + if base.kind == .Array { + children := ct_child_slice(state, base) + if index < 0 || index >= len(children) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime array index out of bounds") + } + return children[index], ct_flow(.Normal), true + } + if base.kind == .String { + if base.index >= u64(len(checker.ast_module.strings)) || index < 0 || index >= len(checker.ast_module.strings[base.index]) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime string index out of bounds") + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=types.U8, integer=i128(checker.ast_module.strings[base.index][index])}), ct_flow(.Normal), true + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "indexing requires a comptime array or string") +} + +ct_unwrap_optional :: proc(state: ^Ct_State, id: Ct_Value_Id, 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 + } + value := state.values[id] + if value.kind == .Optional_Some { + children := ct_child_slice(state, value) + if len(children) > 0 { + return children[0], ct_flow(.Normal), true + } + } + if value.kind == .None { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime optional unwrap of none") + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "postfix '?' requires an optional") +} + +ct_eval_unary :: proc(state: ^Ct_State, op: ast.Expr_Kind, id: Ct_Value_Id, 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 + } + value := state.values[id] + if op == .Not { + if value.kind != .Bool { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "'!' requires a bool operand") + } + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if value.integer == 0 else 0}), ct_flow(.Normal), true + } + if value.kind == .Integer { + result, overflow := intrinsics.overflow_sub(i128(0), value.integer) + if overflow { + state.error = .Overflow + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + value.integer = result + return ct_add_value(state, value), ct_flow(.Normal), true + } + if value.kind == .Float { + value.float = -value.float + return ct_add_value(state, value), ct_flow(.Normal), true + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "negation requires a signed integer or float") +} + +ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, 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] + is_compare := op == .Eq || op == .Ne || op == .Lt || op == .Le || op == .Gt || op == .Ge + if left.kind == .Bool && right.kind == .Bool { + if op != .Eq && op != .Ne { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "bool values only support '==' and '!='") + } + ok := left.integer == right.integer + if op == .Ne { + ok = !ok + } + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if ok else 0}), ct_flow(.Normal), true + } + if left.kind == .Float || right.kind == .Float { + lf := left.float if left.kind == .Float else f64(left.integer) + rf := right.float if right.kind == .Float else f64(right.integer) + if is_compare { + ok := false + #partial switch op { + case .Eq: ok = lf == rf + case .Ne: ok = lf != rf + case .Lt: ok = lf < rf + case .Le: ok = lf <= rf + case .Gt: ok = lf > rf + case .Ge: ok = lf >= rf + } + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if ok else 0}), ct_flow(.Normal), true + } + result := lf + #partial switch op { + case .Add: result = lf + rf + case .Sub: result = lf - rf + case .Mul: result = lf * rf + case .Div: result = lf / rf + } + result_type := types.widest(left.type, right.type) + if !types.is_float(result_type, state.checker.target) { + result_type = types.F64 + } + return ct_add_value(state, Ct_Value{kind=.Float, type=result_type, float=result}), ct_flow(.Normal), true + } + if ct_is_integer_like(left) && ct_is_integer_like(right) { + if is_compare { + ok := false + #partial switch op { + case .Eq: ok = left.integer == right.integer + case .Ne: ok = left.integer != right.integer + case .Lt: ok = left.integer < right.integer + case .Le: ok = left.integer <= right.integer + case .Gt: ok = left.integer > right.integer + case .Ge: ok = left.integer >= right.integer + } + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if ok else 0}), ct_flow(.Normal), true + } + value: i128 + overflow := false + #partial switch op { + case .Sub: + 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) + } + if overflow { + state.error = .Overflow + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + result_type := types.widest(left.type, right.type) + if !types.is_concrete_integer(result_type) && !types.is_enum(result_type, &state.checker.module.types) { + result_type = ct_default_integer_type(value) + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=result_type, integer=value}), ct_flow(.Normal), true + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime binary expression requires compatible operands") +} + +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 + } + value := state.values[id] + if !types.is_concrete_scalar(target) || types.is_bool(target) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "scalar cast requires numeric scalar types") + } + if value.kind == .Float { + if types.is_float(target, state.checker.target) { + value.type = target + return ct_add_value(state, value), ct_flow(.Normal), true + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=target, integer=i128(value.float)}), ct_flow(.Normal), true + } + if ct_is_integer_like(value) { + if types.is_float(target, state.checker.target) { + return ct_add_value(state, Ct_Value{kind=.Float, type=target, float=f64(value.integer)}), ct_flow(.Normal), true + } + value.kind = .Integer + value.type = target + return ct_add_value(state, value), ct_flow(.Normal), true + } + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "scalar cast requires numeric scalar types") +} + +ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + if expr.left != ast.INVALID_EXPR { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "function-pointer calls are not supported in comptime evaluation yet") + } + 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") + } + template := find_template(checker, expr.name, target_pkg) + if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name)) + } + function := checker.ast_module.functions[template] + if function.c_abi || !function.has_body || len(function.unsupported_reason) > 0 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "function '%s' is runtime-only", symbol_text(checker, expr.name)) + } + if !valid_call_arity(function, len(expr.args)) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "function '%s' arity mismatch", symbol_text(checker, expr.name)) + } + comptime_values, comptime_ok := collect_comptime_values(checker, function, expr.args, state.pkg, state.file, false, checker.current_comptime_values) + defer delete(comptime_values, checker.allocator) + if !comptime_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "invalid comptime argument for '%s'", symbol_text(checker, expr.name)) + } + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = comptime_values + defer checker.current_comptime_values = previous_comptime + + result_type := function_channel_type(checker, function) + call_state := ct_state_make(checker, function.pkg, function.file, result_type, comptime_values, diagnose=!state.silent) + defer ct_state_destroy(&call_state) + call_state.steps = state.steps + for param, index in function.params { + if param.comptime_value { + continue + } + param_type := type_from_syntax(checker, param.type, function.pkg, function.file) + if index >= len(expr.args) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + value, flow, ok := ct_eval_expr(state, expr.args[index], param_type, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + value, ok = ct_coerce_value(state, value, param_type, checker.ast_module.exprs[expr.args[index]].span) + if !ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + copied := ct_clone_value(&call_state, state, value) + append(&call_state.bindings, Ct_Binding{name=param.name, type=param_type, value=copied, mutable=false}) + } + flow, ok := ct_exec_statements(&call_state, function.body, false, depth+1) + state.steps = call_state.steps + if !ok { + state.error = call_state.error + state.diagnostic = call_state.diagnostic + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + if flow.kind != .Return { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "comptime function '%s' did not return a value", symbol_text(checker, expr.name)) + } + result := ct_clone_value(state, &call_state, flow.value) + if types.is_valid(expected) { + return ct_coerce_expr_value(state, result, expected, expr.span) + } + return result, ct_flow(.Normal), true +} + +ct_clone_value :: proc(dst, src: ^Ct_State, id: Ct_Value_Id) -> Ct_Value_Id { + if id == INVALID_CT_VALUE || int(id) >= len(src.values) { + return INVALID_CT_VALUE + } + value := src.values[id] + children := ct_child_slice(src, value) + if len(children) > 0 { + value.start = u32(len(dst.children)) + for child in children { + append(&dst.children, ct_clone_value(dst, src, child)) + } + } + return ct_add_value(dst, value) +} + +ct_eval_try_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + channel, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + if channel == INVALID_CT_VALUE || int(channel) >= len(state.values) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + value := state.values[channel] + if value.kind != .Fallible { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "'try' requires a fallible expression") + } + children := ct_child_slice(state, value) + payload := INVALID_CT_VALUE + if len(children) > 0 { + payload = children[0] + } + if value.active == 0 { + return payload, ct_flow(.Normal), true + } + enclosing_success := types.fallible_success(state.result, &checker.module.types) + enclosing_error := types.fallible_error(state.result, &checker.module.types) + if !types.is_valid(enclosing_success) || !types.is_valid(enclosing_error) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "'try' requires an enclosing fallible function") + } + error_payload := payload + if payload != INVALID_CT_VALUE { + coerce_ok: bool + error_payload, coerce_ok = ct_coerce_value(state, payload, enclosing_error, expr.span) + if !coerce_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + } + start := u32(len(state.children)) + append(&state.children, error_payload) + result := ct_add_value(state, Ct_Value{kind=.Fallible, type=state.result, start=start, count=1, active=1}) + return INVALID_CT_VALUE, ct_flow(.Return, result), true +} + +ct_eval_catch_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { + checker := state.checker + channel, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + value := state.values[channel] + if value.kind != .Fallible { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "'catch' requires a fallible expression") + } + success := types.fallible_success(value.type, &checker.module.types) + children := ct_child_slice(state, value) + payload := INVALID_CT_VALUE + if len(children) > 0 { + payload = children[0] + } + if value.active == 0 { + return payload, ct_flow(.Normal), true + } + if expr.right != ast.INVALID_EXPR { + return ct_eval_expr(state, expr.right, success, depth+1) + } + scope_start := len(state.bindings) + if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && payload != INVALID_CT_VALUE { + error_type := types.fallible_error(value.type, &checker.module.types) + append(&state.bindings, Ct_Binding{name=expr.name, type=error_type, value=payload, mutable=false}) + } + handler, handler_ok := ct_exec_statements(state, expr.body, true, depth+1) + resize(&state.bindings, scope_start) + if !handler_ok { + return INVALID_CT_VALUE, handler, false + } + if handler.kind == .Yield { + if types.is_valid(expected) { + return ct_coerce_expr_value(state, handler.value, expected, expr.span) + } + return handler.value, ct_flow(.Normal), true + } + return INVALID_CT_VALUE, handler, ct_fail(state, .Not_Comptime, expr.span, "catch block must yield a value") +} + +ct_make_fallible :: proc(state: ^Ct_State, result_type: types.Type, payload: Ct_Value_Id, error_path: bool) -> Ct_Value_Id { + start := u32(len(state.children)) + append(&state.children, payload) + return ct_add_value(state, Ct_Value{ + kind=.Fallible, type=result_type, start=start, count=1, active=1 if error_path else 0, + }) +} + +ct_return_value :: proc(state: ^Ct_State, expr_id: ast.Expr_Id, span: source.Span, depth: int) -> (Ct_Flow, bool) { + checker := state.checker + if types.is_void(state.result) { + if expr_id != ast.INVALID_EXPR { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "void function cannot return a value") + } + return ct_flow(.Return, ct_add_value(state, Ct_Value{kind=.Void, type=types.VOID})), true + } + if types.kind(state.result, &checker.module.types) == .Fallible { + success := types.fallible_success(state.result, &checker.module.types) + error_type := types.fallible_error(state.result, &checker.module.types) + if expr_id == ast.INVALID_EXPR { + if types.is_void(success) { + return ct_flow(.Return, ct_make_fallible(state, state.result, INVALID_CT_VALUE, false)), true + } + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "fallible return requires a value") + } + expr := checker.ast_module.exprs[expr_id] + error_path := false + expected := success + if expr.kind == .Enum_Literal && types.sum_has_name(&checker.module.types, error_type, u32(expr.name)) { + error_path = true + expected = error_type + } + value, flow, ok := ct_eval_expr(state, expr_id, expected, depth+1) + if !ok || flow.kind != .Normal { + return flow, ok + } + value, ok = ct_coerce_value(state, value, expected, span) + if !ok { + return ct_flow(.Normal), false + } + return ct_flow(.Return, ct_make_fallible(state, state.result, value, error_path)), true + } + if expr_id == ast.INVALID_EXPR { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "non-void function must return a value") + } + value, flow, ok := ct_eval_expr(state, expr_id, state.result, depth+1) + if !ok || flow.kind != .Normal { + return flow, ok + } + value, ok = ct_coerce_value(state, value, state.result, span) + if !ok { + return ct_flow(.Normal), false + } + return ct_flow(.Return, value), true +} + +ct_exec_statements :: proc( + state: ^Ct_State, + statements: []ast.Stmt_Id, + yield_returns: bool, + depth := 0, +) -> (Ct_Flow, bool) { + checker := state.checker + if depth > 128 { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, source.Span{}, "comptime evaluation exceeded recursion depth") + } + scope_start := len(state.bindings) + defer_start := len(state.defers) + defer { + resize(&state.bindings, scope_start) + resize(&state.defers, defer_start) + } + for statement_id in statements { + if statement_id == ast.INVALID_STMT || int(statement_id) >= len(checker.ast_module.statements) { + return ct_flow(.Normal), false + } + statement := checker.ast_module.statements[statement_id] + if !ct_step(state, statement.span) { + return ct_flow(.Normal), false + } + flow := ct_flow(.Normal) + ok := true + #partial switch statement.kind { + case .Declaration: + if statement.expr == ast.INVALID_EXPR { + value_flow, value_ok := ct_exec_statements(state, statement.body, true, depth+1) + ok = value_ok + if ok && value_flow.kind == .Yield { + value := value_flow.value + declared := type_from_syntax(checker, statement.type, state.pkg, state.file) + if types.is_valid(declared) && !types.is_void(declared) { + value, ok = ct_coerce_value(state, value, declared, statement.span) + } + if ok && statement.name != checker.sink_symbol { + append(&state.bindings, Ct_Binding{name=statement.name, type=state.values[value].type, value=value, mutable=!statement.immutable}) + } + } else if ok { + ok = ct_fail(state, .Not_Comptime, statement.span, "comptime value block must yield") + } + } else { + declared := type_from_syntax(checker, statement.type, state.pkg, state.file) + expected := declared if types.is_valid(declared) && !types.is_void(declared) else types.INVALID + value, expr_flow, expr_ok := ct_eval_expr(state, statement.expr, expected, depth+1) + ok = expr_ok + flow = expr_flow + if ok && flow.kind == .Normal { + if types.is_valid(expected) { + value, ok = ct_coerce_value(state, value, expected, statement.span) + } + if ok && statement.name != checker.sink_symbol { + append(&state.bindings, Ct_Binding{name=statement.name, type=state.values[value].type, value=value, mutable=!statement.immutable}) + } + } + } + case .Assignment: + flow, ok = ct_exec_assignment(state, statement, depth+1) + case .Expression: + _, flow, ok = ct_eval_expr(state, statement.expr, types.INVALID, depth+1) + case .Return: + if yield_returns { + ok = ct_fail(state, .Not_Comptime, statement.span, "'return' is not valid in this comptime block") + } else if statement.value_control_flow { + value_flow, value_ok := ct_exec_statements(state, statement.body, true, depth+1) + ok = value_ok + if ok && value_flow.kind == .Yield { + value := value_flow.value + if types.kind(state.result, &checker.module.types) == .Fallible { + success := types.fallible_success(state.result, &checker.module.types) + value, ok = ct_coerce_value(state, value, success, statement.span) + if ok { + flow = ct_flow(.Return, ct_make_fallible(state, state.result, value, false)) + } + } else { + value, ok = ct_coerce_value(state, value, state.result, statement.span) + if ok { + flow = ct_flow(.Return, value) + } + } + } + } else { + flow, ok = ct_return_value(state, statement.expr, statement.span, depth+1) + } + case .Yield: + if !yield_returns { + ok = ct_fail(state, .Not_Comptime, statement.span, "'yield' is only valid in a comptime value block") + } else if statement.value_control_flow { + flow, ok = ct_exec_statements(state, statement.body, true, depth+1) + } else { + value, expr_flow, expr_ok := ct_eval_expr(state, statement.expr, types.INVALID, depth+1) + ok = expr_ok + flow = expr_flow + if ok && flow.kind == .Normal { + flow = ct_flow(.Yield, value, statement.label) + } + } + case .If: + flow, ok = ct_exec_if(state, statement, yield_returns, depth+1) + case .While: + flow, ok = ct_exec_while(state, statement, yield_returns, depth+1) + case .For: + flow, ok = ct_exec_for(state, statement, yield_returns, depth+1) + case .Break: + flow = ct_flow(.Break, INVALID_CT_VALUE, statement.label) + case .Continue: + flow = ct_flow(.Continue, INVALID_CT_VALUE, statement.label) + case .Block: + flow, ok = ct_exec_statements(state, statement.body, yield_returns, depth+1) + case .Defer: + append(&state.defers, statement.update) + case .Match: + flow, ok = ct_exec_match(state, statement, yield_returns, depth+1) + case .Match_Arm: + ok = ct_fail(state, .Not_Comptime, statement.span, "unexpected match arm outside 'match'") + case .Invalid: + ok = false + } + if !ok { + return flow, false + } + if flow.kind != .Normal { + if !ct_flush_defers(state, defer_start, depth+1) { + return flow, false + } + return flow, true + } + } + if !ct_flush_defers(state, defer_start, depth+1) { + return ct_flow(.Normal), false + } + return ct_flow(.Normal), true +} + +ct_flush_defers :: proc(state: ^Ct_State, start: int, depth: int) -> bool { + for index := len(state.defers) - 1; index >= start; index -= 1 { + stmt := [1]ast.Stmt_Id{state.defers[index]} + flow, ok := ct_exec_statements(state, stmt[:], false, depth+1) + if !ok || flow.kind != .Normal { + return false + } + } + return true +} + +ct_exec_assignment :: proc(state: ^Ct_State, statement: ast.Stmt, depth: int) -> (Ct_Flow, bool) { + checker := state.checker + name := statement.name + if statement.target != ast.INVALID_EXPR { + target := checker.ast_module.exprs[statement.target] + if target.kind == .Name && !symbol.is_valid(target.qualifier) { + name = target.name + } else { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "comptime assignment only supports local targets in V1") + } + } + if name == checker.sink_symbol { + if statement.expr == ast.INVALID_EXPR { + flow, ok := ct_exec_statements(state, statement.body, true, depth+1) + return ct_flow(.Normal), ok && (flow.kind == .Yield || flow.kind == .Normal) + } + _, flow, ok := ct_eval_expr(state, statement.expr, types.INVALID, depth+1) + return flow, ok + } + index, found := ct_find_binding_index(state, name) + if !found { + return ct_flow(.Normal), ct_failf(state, .Not_Comptime, statement.span, "cannot assign unresolved comptime local '%s'", symbol_text(checker, name)) + } + if !state.bindings[index].mutable { + return ct_flow(.Normal), ct_failf(state, .Not_Comptime, statement.span, "cannot assign immutable comptime local '%s'", symbol_text(checker, name)) + } + expected := state.bindings[index].type + value := INVALID_CT_VALUE + flow := ct_flow(.Normal) + ok := true + if statement.expr == ast.INVALID_EXPR { + flow, ok = ct_exec_statements(state, statement.body, true, depth+1) + if ok && flow.kind == .Yield { + value = flow.value + flow = ct_flow(.Normal) + } + } else { + value, flow, ok = ct_eval_expr(state, statement.expr, expected, depth+1) + } + if !ok || flow.kind != .Normal { + return flow, ok + } + if statement.assignment_op != .Set { + current := state.bindings[index].value + op := ast.Expr_Kind.Add + #partial switch statement.assignment_op { + case .Sub: op = .Sub + case .Mul: op = .Mul + case .Div: op = .Div + case: op = .Add + } + bin_flow: Ct_Flow + value, bin_flow, ok = ct_eval_binary(state, op, current, value, statement.span) + if !ok || bin_flow.kind != .Normal { + return bin_flow, ok + } + } + value, ok = ct_coerce_value(state, value, expected, statement.span) + if !ok { + return ct_flow(.Normal), false + } + state.bindings[index].value = value + state.bindings[index].type = state.values[value].type + return ct_flow(.Normal), true +} + +ct_exec_if :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, depth: int) -> (Ct_Flow, bool) { + checker := state.checker + if len(statement.captures) == 0 { + condition, flow, ok := ct_eval_expr(state, statement.expr, types.BOOL, depth+1) + if !ok || flow.kind != .Normal { + return flow, ok + } + value, bool_ok := ct_bool_value(state, condition) + if !bool_ok { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' condition must be a bool") + } + body := statement.body if value else statement.else_body + return ct_exec_statements(state, body, yield_returns, depth+1) + } + operands: [dynamic]ast.Expr_Id + operands.allocator = checker.allocator + defer delete(operands) + flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands) + if len(operands) != len(statement.captures) { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' unwrap capture count mismatch") + } + scope_start := len(state.bindings) + matched := true + for operand, index in operands { + value, flow, ok := ct_eval_expr(state, operand, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + resize(&state.bindings, scope_start) + return flow, ok + } + v := state.values[value] + if v.kind == .None { + matched = false + break + } + if v.kind != .Optional_Some { + resize(&state.bindings, scope_start) + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' unwrap requires an optional value") + } + children := ct_child_slice(state, v) + if len(children) > 0 && statement.captures[index] != checker.sink_symbol { + append(&state.bindings, Ct_Binding{name=statement.captures[index], type=state.values[children[0]].type, value=children[0], mutable=false}) + } + } + if matched && statement.guard != ast.INVALID_EXPR { + guard, flow, ok := ct_eval_expr(state, statement.guard, types.BOOL, depth+1) + if !ok || flow.kind != .Normal { + resize(&state.bindings, scope_start) + return flow, ok + } + guard_value, guard_ok := ct_bool_value(state, guard) + if !guard_ok { + resize(&state.bindings, scope_start) + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' unwrap guard must be a bool") + } + matched = guard_value + } + body := statement.body if matched else statement.else_body + flow, ok := ct_exec_statements(state, body, yield_returns, depth+1) + resize(&state.bindings, scope_start) + return flow, ok +} + +ct_loop_consumes_flow :: proc(flow: Ct_Flow, label: symbol.Id, want_continue: bool) -> bool { + if want_continue && flow.kind != .Continue { + return false + } + if !want_continue && flow.kind != .Break { + return false + } + return !symbol.is_valid(flow.label) || flow.label == label +} + +ct_exec_while :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, depth: int) -> (Ct_Flow, bool) { + for { + if !ct_step(state, statement.span) { + return ct_flow(.Normal), false + } + condition, flow, ok := ct_eval_expr(state, statement.expr, types.BOOL, depth+1) + if !ok || flow.kind != .Normal { + return flow, ok + } + value, bool_ok := ct_bool_value(state, condition) + if !bool_ok { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'while' condition must be a bool") + } + if !value { + return ct_flow(.Normal), true + } + body_flow, body_ok := ct_exec_statements(state, statement.body, yield_returns, depth+1) + if !body_ok { + return body_flow, false + } + if ct_loop_consumes_flow(body_flow, statement.label, false) { + return ct_flow(.Normal), true + } + if body_flow.kind != .Normal && !ct_loop_consumes_flow(body_flow, statement.label, true) { + return body_flow, true + } + if statement.update != ast.INVALID_STMT { + update := [1]ast.Stmt_Id{statement.update} + update_flow, update_ok := ct_exec_statements(state, update[:], false, depth+1) + if !update_ok || update_flow.kind != .Normal { + return update_flow, update_ok + } + } + } +} + +ct_exec_for :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, depth: int) -> (Ct_Flow, bool) { + checker := state.checker + if statement.pointer_capture { + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "comptime for-loops do not support pointer captures") + } + iterable, flow, ok := ct_eval_expr(state, statement.expr, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return flow, ok + } + value := state.values[iterable] + if value.kind == .Range { + children := ct_child_slice(state, value) + if len(children) < 2 { + return ct_flow(.Normal), false + } + start, start_ok := ct_integer_value(state, children[0]) + end, end_ok := ct_integer_value(state, children[1]) + if !start_ok || !end_ok { + return ct_flow(.Normal), false + } + index := i128(0) + for current := start; current < end || (value.active != 0 && current == end); current += 1 { + scope_start := len(state.bindings) + item := ct_add_value(state, Ct_Value{kind=.Integer, type=types.child_type(value.type, &checker.module.types), integer=current}) + append(&state.bindings, Ct_Binding{name=statement.name, type=state.values[item].type, value=item, mutable=false}) + if symbol.is_valid(statement.index_name) { + idx := ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=index}) + append(&state.bindings, Ct_Binding{name=statement.index_name, type=types.USIZE, value=idx, mutable=false}) + } + body_flow, body_ok := ct_exec_statements(state, statement.body, yield_returns, depth+1) + resize(&state.bindings, scope_start) + if !body_ok { + return body_flow, false + } + if ct_loop_consumes_flow(body_flow, statement.label, false) { + return ct_flow(.Normal), true + } + if body_flow.kind != .Normal && !ct_loop_consumes_flow(body_flow, statement.label, true) { + return body_flow, true + } + index += 1 + } + return ct_flow(.Normal), true + } + if value.kind == .Array { + children := ct_child_slice(state, value) + for child, index in children { + scope_start := len(state.bindings) + append(&state.bindings, Ct_Binding{name=statement.name, type=state.values[child].type, value=child, mutable=false}) + if symbol.is_valid(statement.index_name) { + idx := ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(index)}) + append(&state.bindings, Ct_Binding{name=statement.index_name, type=types.USIZE, value=idx, mutable=false}) + } + body_flow, body_ok := ct_exec_statements(state, statement.body, yield_returns, depth+1) + resize(&state.bindings, scope_start) + if !body_ok { + return body_flow, false + } + if ct_loop_consumes_flow(body_flow, statement.label, false) { + return ct_flow(.Normal), true + } + if body_flow.kind != .Normal && !ct_loop_consumes_flow(body_flow, statement.label, true) { + return body_flow, true + } + } + return ct_flow(.Normal), true + } + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "comptime for-loop iterable must be a range or array") +} + +ct_exec_match :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, depth: int) -> (Ct_Flow, bool) { + checker := state.checker + subject, flow, ok := ct_eval_expr(state, statement.expr, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return flow, ok + } + subject_value := state.values[subject] + for arm_id in statement.body { + arm := checker.ast_module.statements[arm_id] + if arm.kind != .Match_Arm { + continue + } + matched := len(arm.patterns) == 0 + payload := INVALID_CT_VALUE + if !matched { + for pattern_id in arm.patterns { + pattern := checker.ast_module.exprs[pattern_id] + if subject_value.kind == .Struct && types.is_tagged_union(subject_value.type, &checker.module.types) { + if pattern.kind != .Enum_Literal { + continue + } + if field_index, _, found := find_struct_field(checker, subject_value.type, pattern.name); found && field_index == int(subject_value.active) { + matched = true + children := ct_child_slice(state, subject_value) + if len(children) > 0 { + payload = children[0] + } + break + } + } else if pattern.kind == .Range { + probe, range_flow, range_ok := ct_eval_expr(state, pattern_id, subject_value.type, depth+1) + if range_ok && range_flow.kind == .Normal && ct_range_contains(state, probe, subject) { + matched = true + break + } + } else { + probe, pattern_flow, pattern_ok := ct_eval_expr(state, pattern_id, subject_value.type, depth+1) + if pattern_ok && pattern_flow.kind == .Normal && ct_values_equal(state, subject, probe) { + matched = true + break + } + } + } + } + if !matched { + continue + } + scope_start := len(state.bindings) + if len(arm.captures) > 0 && payload != INVALID_CT_VALUE { + capture := arm.captures[0] + if arm.pointer_capture { + resize(&state.bindings, scope_start) + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, arm.span, "comptime match does not support pointer captures") + } + if capture != checker.sink_symbol { + append(&state.bindings, Ct_Binding{name=capture, type=state.values[payload].type, value=payload, mutable=false}) + } + } + if yield_returns && len(arm.body) == 1 && checker.ast_module.statements[arm.body[0]].kind == .Expression { + expr_stmt := checker.ast_module.statements[arm.body[0]] + value, expr_flow, expr_ok := ct_eval_expr(state, expr_stmt.expr, types.INVALID, depth+1) + resize(&state.bindings, scope_start) + if !expr_ok || expr_flow.kind != .Normal { + return expr_flow, expr_ok + } + return ct_flow(.Yield, value), true + } + arm_flow, arm_ok := ct_exec_statements(state, arm.body, yield_returns, depth+1) + resize(&state.bindings, scope_start) + return arm_flow, arm_ok + } + return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "comptime match did not select an arm") +} + +ct_values_equal :: proc(state: ^Ct_State, left_id, right_id: Ct_Value_Id) -> 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 false + } + left := state.values[left_id] + right := state.values[right_id] + if ct_is_integer_like(left) && ct_is_integer_like(right) { + return left.integer == right.integer + } + if left.kind == .Float && right.kind == .Float { + return left.float == right.float + } + if left.kind == .Bool && right.kind == .Bool { + return left.integer == right.integer + } + return false +} + +ct_range_contains :: proc(state: ^Ct_State, range_id, value_id: Ct_Value_Id) -> bool { + if range_id == INVALID_CT_VALUE || int(range_id) >= len(state.values) { + return false + } + range_value := state.values[range_id] + if range_value.kind != .Range { + return false + } + children := ct_child_slice(state, range_value) + if len(children) < 2 { + return false + } + lo, lo_ok := ct_integer_value(state, children[0]) + hi, hi_ok := ct_integer_value(state, children[1]) + value, value_ok := ct_integer_value(state, value_id) + if !lo_ok || !hi_ok || !value_ok { + return false + } + return value >= lo && (value <= hi if range_value.active != 0 else value < hi) +} + +eval_integer_constant_in_context :: proc( + checker: ^Checker, + expr_id: ast.Expr_Id, + pkg: ast.Package_Id, + file: ast.File_Id, + depth := 0, + values: []Comptime_Value = nil, +) -> Constant { + if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return Constant{kind=.Not_Constant} + } + state := ct_state_make(checker, pkg, file, types.INVALID, values, diagnose=false) + defer ct_state_destroy(&state) + value, flow, ok := ct_eval_expr(&state, expr_id, types.INVALID, depth) + if !ok || flow.kind != .Normal { + #partial switch state.error { + case .Overflow: + return Constant{kind=.Overflow} + case .Div_By_Zero: + return Constant{kind=.Div_By_Zero} + } + return Constant{kind=.Not_Constant} + } + integer, integer_ok := ct_integer_value(&state, value) + if !integer_ok { + return Constant{kind=.Not_Constant} + } + return Constant{kind=.Value, value=integer} +} + +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) { + state := ct_state_make(checker, pkg, file, types.INVALID, values, diagnose=false) + defer ct_state_destroy(&state) + flow, ok := ct_exec_statements(&state, statements, yield_returns, depth) + if !ok { + #partial switch state.error { + case .Overflow: + return Constant{kind=.Overflow}, false, false + case .Div_By_Zero: + return Constant{kind=.Div_By_Zero}, false, false + } + return Constant{kind=.Not_Constant}, false, false + } + wanted := Ct_Flow_Kind.Yield if yield_returns else Ct_Flow_Kind.Return + if flow.kind != wanted { + return Constant{kind=.Not_Constant}, false, true + } + integer, integer_ok := ct_integer_value(&state, flow.value) + if !integer_ok { + return Constant{kind=.Not_Constant}, false, false + } + return Constant{kind=.Value, value=integer}, true, true +} + +eval_comptime_call :: proc( + checker: ^Checker, + expr: ast.Expr, + pkg: ast.Package_Id, + file: ast.File_Id, + depth: int, + values: []Comptime_Value, +) -> Constant { + state := ct_state_make(checker, pkg, file, types.INVALID, values, diagnose=false) + defer ct_state_destroy(&state) + value, flow, ok := ct_eval_call_expr(&state, expr, types.INVALID, depth) + if !ok || flow.kind != .Normal { + #partial switch state.error { + case .Overflow: + return Constant{kind=.Overflow} + case .Div_By_Zero: + return Constant{kind=.Div_By_Zero} + } + return Constant{kind=.Not_Constant} + } + integer, integer_ok := ct_integer_value(&state, value) + if !integer_ok { + return Constant{kind=.Not_Constant} + } + return Constant{kind=.Value, value=integer} +} + +infer_comptime_expr_type :: proc( + checker: ^Checker, + expr: ast.Expr, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> types.Type { + state := ct_state_make(checker, pkg, file, diagnose=false) + defer ct_state_destroy(&state) + value := INVALID_CT_VALUE + flow := ct_flow(.Normal) + ok := false + if expr.left != ast.INVALID_EXPR { + value, flow, ok = ct_eval_expr(&state, expr.left, types.INVALID) + } else { + flow, ok = ct_exec_statements(&state, expr.body, true) + if ok && flow.kind == .Yield { + value = flow.value + flow = ct_flow(.Normal) + } else if ok { + state.error = .Not_Comptime + if !state.silent { + state.diagnostic = source.add(checker.diagnostics, expr.span, "comptime block must yield a value") + } + ok = false + } + } + if !ok || flow.kind != .Normal || value == INVALID_CT_VALUE || int(value) >= len(state.values) { + if state.error == .Overflow || state.error == .Div_By_Zero { + return types.I64 + } + return types.INVALID + } + return state.values[value].type +} + +build_comptime_expr :: proc( + checker: ^Checker, + expr: ast.Expr, + expected: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> hir.Expr_Id { + state := ct_state_make(checker, pkg, file) + defer ct_state_destroy(&state) + value := INVALID_CT_VALUE + flow := ct_flow(.Normal) + ok := false + if expr.left != ast.INVALID_EXPR { + value, flow, ok = ct_eval_expr(&state, expr.left, expected) + } else { + flow, ok = ct_exec_statements(&state, expr.body, true) + if ok && flow.kind == .Yield { + value = flow.value + flow = ct_flow(.Normal) + } else if ok { + state.error = .Not_Comptime + state.diagnostic = source.add(checker.diagnostics, expr.span, "comptime block must yield a value") + ok = false + } + } + if ok && flow.kind == .Normal && value != INVALID_CT_VALUE { + return ct_materialize_value(&state, value, expr.span, expected) + } + if state.error == .Div_By_Zero { + return build_constant_expr(checker, expr, Constant{kind=.Div_By_Zero}, expected) + } + if state.error == .Overflow { + return build_constant_expr(checker, expr, Constant{kind=.Overflow}, expected) + } + diagnostic := state.diagnostic + if diagnostic == source.INVALID_DIAGNOSTIC { + diagnostic = source.add(checker.diagnostics, expr.span, "expression cannot be evaluated at comptime") + } + return invalid_hir_expr(checker, expr.span, diagnostic, expected) +} diff --git a/compiler_tests.odin b/compiler_tests.odin index d5a17c4..ac98d6a 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2338,26 +2338,25 @@ main func() i32 { } @(test) -comptime_expression_diagnoses_unsupported_v1_evaluation :: proc(t: ^testing.T) { - text := `loop func() int { +comptime_expression_diagnoses_runtime_only_and_quota :: proc(t: ^testing.T) { + text := `native c_func() i32 +spin func() i32 { while true { - return 1 } return 0 } -missing func() int { +missing func() i32 { if true { } } -recurse func(value int) int { - return recurse(value) -} +GLOBAL :: 1 main func() void { runtime i32 = 1 _ = $runtime - _ = $loop() + _ = $native() + _ = $&GLOBAL + _ = $spin() _ = $missing() - _ = $recurse(1) _ = ${ value :: 1 } @@ -2375,13 +2374,27 @@ main func() void { hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) - found := 0 + found_runtime := false + found_external := false + found_pointer := false + found_quota := false + found_missing := false + found_yield := false for diagnostic in diagnostics.items { - if strings.contains(diagnostic.message, "expression cannot be evaluated at comptime") { - found += 1 - } + message := diagnostic.message + found_runtime = found_runtime || strings.contains(message, "unresolved comptime value 'runtime'") + found_external = found_external || strings.contains(message, "runtime-only") + found_pointer = found_pointer || strings.contains(message, "pointers and slices are not supported") + found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota") + found_missing = found_missing || strings.contains(message, "did not return a value") + found_yield = found_yield || strings.contains(message, "comptime block must yield a value") } - testing.expect(t, found >= 5) + testing.expect(t, found_runtime) + testing.expect(t, found_external) + testing.expect(t, found_pointer) + testing.expect(t, found_quota) + testing.expect(t, found_missing) + testing.expect(t, found_yield) } @(test) @@ -5774,6 +5787,16 @@ comptime_eval_compile_and_run :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 0) } +@(test) +comptime_v1_compile_and_run :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-comptime-v1" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/comptime_v1", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + @(test) lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-lazy-import" diff --git a/examples/programs/comptime_v1/main.bro b/examples/programs/comptime_v1/main.bro new file mode 100644 index 0000000..4e23e13 --- /dev/null +++ b/examples/programs/comptime_v1/main.bro @@ -0,0 +1,138 @@ +State :: enum { + idle + ready + failed +} + +Point :: struct { + x i32 + y i32 +} + +Box :: union(enum) { + point Point + empty void +} + +Error :: enum { + bad +} + +make_point func() Point { + return Point { x = 3, y = 4 } +} + +sum_loop func(limit i32) i32 { + total i32 = 0 + i i32 = 0 + while i < limit { + i += 1 + if i == 2 { + continue + } + total += i + } + return total +} + +sum_for func() i32 { + total i32 = 0 + values [_]i32 = [1, 2, 3] + for values |value, index| { + total += value + index + } + return total +} + +defer_value func() i32 { + value i32 = 1 + { + defer value += 10 + value += 1 + } + return value +} + +describe func(box Box) i32 { + return match box { + .point |p|: p.x + p.y + .empty: 0 + } +} + +maybe func(flag bool) ?i32 { + if flag { + return 9 + } + return none +} + +may_fail func(flag bool) i32 ! Error { + if flag { + return .bad + } + return 7 +} + +use_try func() i32 ! Error { + value :: try may_fail(false) + return value + 1 +} + +recover func() i32 { + return may_fail(true) catch |e| { + match e { + .bad: yield 5 + } + } +} + +GLOBAL :: $sum_loop(4) + +main func() i32 { + point Point :: $make_point() + numbers [_]i32 :: $[4, 5, 6] + box Box :: $Box { point = Point { x = 8, y = 1 } } + state State :: $State.ready + name :: $"bro" + value i32 :: $sum_for() + deferred i32 :: $defer_value() + optional i32 :: $maybe(true)? + tried i32 :: $use_try() catch 0 + recovered i32 :: $recover() + + if point.x + point.y != 7 { + return 1 + } + if numbers.len != 3 or numbers[2] != 6 { + return 2 + } + if describe(box) != 9 { + return 3 + } + if state != State.ready { + return 4 + } + if name.len != 3 { + return 5 + } + if value != 9 { + return 6 + } + if deferred != 12 { + return 7 + } + if optional != 9 { + return 8 + } + if tried != 8 { + return 9 + } + if recovered != 5 { + return 10 + } + if GLOBAL != 8 { + return 11 + } + return 0 +}