From e00a4e929aa858d539eab14d73192eaf6b7bc32e Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 2 Jul 2026 21:31:53 +0200 Subject: [PATCH] comptime eval --- LANGUAGE.md | 3 +- README.md | 1 + TODO.md | 31 ++- compiler/ast/ast.odin | 1 + compiler/checker/checker.odin | 242 +++++++++++++++++++++-- compiler/parser/parser.odin | 50 +++-- compiler_tests.odin | 159 +++++++++++++++ examples/programs/comptime_eval/main.bro | 49 +++++ 8 files changed, 500 insertions(+), 36 deletions(-) create mode 100644 examples/programs/comptime_eval/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index 6f8e8ec..063f0d2 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -54,6 +54,7 @@ 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 - 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 @@ -81,7 +82,7 @@ roadmap and milestone history. ## PLANNED / DEFERRED -- comptime-evaluable functions +- broader Zig-style comptime execution - 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 0932411..5d132d5 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ 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 - 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 2b44e42..52bc1f2 100644 --- a/TODO.md +++ b/TODO.md @@ -651,7 +651,29 @@ inferred type parameters, first-class type values, or comptime execution 27.6 comptime-evaluable constants/functions - - planned shape: `$x :: 32` and `$sum func(a, b int) int { ... }` + - `$expr` forces comptime evaluation of an expression: + `x :: $32`, `n :: $sum(1, 2)`, and `res :: ${ ... }` + - constant contexts such as array counts and comptime value arguments implicitly + require comptime evaluation; ordinary immutable bindings remain ordinary bindings + - ordinary `func` calls are comptime-evaluable when reached from a comptime context; + do not add a separate `$sum func(...)` declaration form + - v1 evaluator supports integer literals/arithmetic, boolean conditions, immutable + locals, `return`, `if`/`else`, comptime blocks, and direct calls to other evaluable + brolang functions + - broader Zig-style comptime execution is milestone 27.7 + +27.7 broader Zig-style comptime execution + - extend the comptime evaluator from integer scalars into a real compile-time value + model: bools, floats, strings, arrays/slices, structs/unions/enums, optionals, + fallibles, and pointers to comptime storage + - support mutable comptime locals/assignment, loops with an evaluation quota, + `defer`, `match`, value blocks/`yield`, and `try`/`catch` + - allow calls through comptime-known function values/function pointers; keep external + `c_func` calls runtime-only unless a future compiler intrinsic explicitly models + their behavior + - immutable locals/globals with comptime-known initializers may feed comptime + evaluation; runtime-dependent values remain invalid in comptime contexts + - no runtime side effects during comptime evaluation 28. brolang build system (requires comptime execution) @@ -1244,9 +1266,10 @@ data :: read_file(path) catch |e| match e { ``` make_array func($N usize) [N]u8 { ... } # implemented: integer comptime value params -max func($T type, a, b T) T { ... } # deferred: comptime type params -$x :: 32 # deferred: comptime evaluable constant -$sum func(a, b int) int { ... } # deferred: comptime evaluable function +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 ``` ## A word on memory allocation diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 385e6df..e65166d 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -85,6 +85,7 @@ Expr_Kind :: enum u8 { Struct_Literal, Keyed, Cast, + Comptime, Negate, Not, Add, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index a2d78cb..c981c46 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -389,6 +389,7 @@ eval_integer_constant_in_context :: proc( 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} @@ -397,8 +398,15 @@ eval_integer_constant_in_context :: proc( #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} @@ -417,17 +425,32 @@ eval_integer_constant_in_context :: proc( 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) + 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) + 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) - right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1) + left := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1, values) + right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1, values) if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero { return Constant{kind = .Div_By_Zero} } @@ -453,10 +476,163 @@ eval_integer_constant_in_context :: proc( 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 @@ -952,6 +1128,7 @@ collect_comptime_values :: proc( pkg: ast.Package_Id, file: ast.File_Id, diagnose := false, + extra_values: []Comptime_Value = nil, ) -> ([]Comptime_Value, bool) { if !function_has_comptime_params(function) { return nil, true @@ -1002,7 +1179,7 @@ collect_comptime_values :: proc( } constant := Constant{kind = .Not_Constant} if index < len(args) { - constant = eval_integer_constant_in_context(checker, args[index], pkg, file) + constant = eval_integer_constant_in_context(checker, args[index], pkg, file, values=extra_values) } if constant.kind != .Value { if diagnose { @@ -1145,6 +1322,11 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as } case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast: append(&stack, expr.left) + case .Comptime: + if expr.left != ast.INVALID_EXPR { + append(&stack, expr.left) + } + mark_block_imports_used(checker, expr.body, file) case .Catch: append(&stack, expr.left) if expr.right != ast.INVALID_EXPR { @@ -1744,6 +1926,25 @@ infer_compound_expr :: proc( ) -> types.Type { 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 case .Bool: return types.BOOL case .Not: @@ -1954,7 +2155,7 @@ infer_expr :: proc( _ = pop(&stack) case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, .Cast, - .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: + .Comptime, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types) _ = pop(&stack) case .Name: @@ -2371,7 +2572,7 @@ infer_statements :: proc( open_float := false const_val := i128(0) if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { - constant := eval_constant(checker, statement.expr) + constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file) if constant.kind == .Value && fits_i64(constant.value) { open = true const_val = constant.value @@ -2842,7 +3043,7 @@ expr_accepts_numeric_demand :: proc( expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false } - if constant := eval_constant(checker, expr_id); constant.kind == .Value { + if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); constant.kind == .Value { return open_integer_accepts_demand(checker, constant.value, demand) } if is_float_constant_expr(checker, expr_id) { @@ -2945,7 +3146,7 @@ infer_all :: proc(checker: ^Checker) { if global.external { continue } - constant := eval_constant(checker, global.expr) + constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file) if constant.kind == .Value && fits_i64(constant.value) { checker.global_open_const[index] = true checker.global_const_value[index] = constant.value @@ -4120,6 +4321,21 @@ build_compound_expr :: proc( target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Comptime: + constant := Constant{kind=.Not_Constant} + if expr.left != ast.INVALID_EXPR { + constant = eval_integer_constant_in_context(checker, expr.left, pkg, file) + } else { + value, produced, ok := eval_comptime_statements(checker, expr.body, pkg, file, 0, nil, true) + if ok && produced { + constant = value + } + } + if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero { + return build_constant_expr(checker, expr, constant, expected) + } + id := source.add(checker.diagnostics, expr.span, "expression cannot be evaluated at comptime") + return invalid_hir_expr(checker, expr.span, id) case .Bool: return add_hir_expr(checker, hir.Expr{ kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer), @@ -4398,9 +4614,9 @@ build_expr :: proc( continue } switch expr.kind { - case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, + case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, - .Bool, .Cast, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, + .Bool, .Cast, .Comptime, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, .Enum_Literal: last = build_compound_expr( checker, expr, locals, global_reads, calls, frame.expected, pkg, file, @@ -5092,7 +5308,7 @@ build_block :: proc( // `undefined`, open numeric constants, or arithmetic expressions. open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr) if open_const_decl { - constant := eval_constant(checker, statement.expr) + constant := eval_integer_constant_in_context(checker, statement.expr, ctx.pkg, ctx.file) open_const_decl = constant.kind == .Value && fits_i64(constant.value) || is_float_constant_expr(checker, statement.expr) } @@ -7650,7 +7866,7 @@ build_globals :: proc(checker: ^Checker) { expected := types.INVALID if is_runtime_type(checker, declared) { expected = declared - } else if constant := eval_constant(checker, global.expr); + } else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file); constant.kind == .Value && fits_i64(constant.value) && is_runtime_type(checker, checker.global_types[global_index]) { // Open integer constant: build against its demanded/defaulted type. Gated diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 60bb1ff..db0f85c 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -914,7 +914,7 @@ is_simple_range_bound :: proc(expr: ast.Expr) -> bool { prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) { #partial switch kind { - case .Minus, .Ampersand, .Bang, .Keyword_Try: + case .Minus, .Ampersand, .Bang, .Dollar, .Keyword_Try: return 20, true } return 0, false @@ -931,24 +931,38 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int left := ast.INVALID_EXPR if right_power, ok := prefix_binding_power(current(parser).kind); ok { operator := advance(parser) - if parser.delimiter_depth > 0 { - skip_newlines(parser) + if operator.kind == .Dollar && current(parser).kind == .Left_Brace { + body := parse_block(parser) + end := previous(parser) + left = add_expr(parser, ast.Expr{ + kind = .Comptime, + span = span_from(operator.span, end.span), + body = body, + left = ast.INVALID_EXPR, + right = ast.INVALID_EXPR, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + } else { + if parser.delimiter_depth > 0 { + skip_newlines(parser) + } + operand := parse_expression_bp(parser, right_power, nesting+1) + operand_expr := parser.module.exprs[operand] + prefix_kind := ast.Expr_Kind.Negate + #partial switch operator.kind { + case .Ampersand: prefix_kind = .Address + case .Bang: prefix_kind = .Not + case .Dollar: prefix_kind = .Comptime + case .Keyword_Try: prefix_kind = .Try + } + left = add_expr(parser, ast.Expr{ + kind = prefix_kind, + span = span_from(operator.span, operand_expr.span), + left = operand, + right = ast.INVALID_EXPR, + diagnostic = source.INVALID_DIAGNOSTIC, + }) } - operand := parse_expression_bp(parser, right_power, nesting+1) - operand_expr := parser.module.exprs[operand] - prefix_kind := ast.Expr_Kind.Negate - #partial switch operator.kind { - case .Ampersand: prefix_kind = .Address - case .Bang: prefix_kind = .Not - case .Keyword_Try: prefix_kind = .Try - } - left = add_expr(parser, ast.Expr{ - kind=prefix_kind, - span=span_from(operator.span, operand_expr.span), - left=operand, - right=ast.INVALID_EXPR, - diagnostic=source.INVALID_DIAGNOSTIC, - }) } else { left = parse_primary(parser, nesting) } diff --git a/compiler_tests.odin b/compiler_tests.odin index 3d8e3cb..d5a17c4 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -567,6 +567,42 @@ main func() void {} testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate) } +@(test) +parser_accepts_comptime_prefix_and_block_expressions :: proc(t: ^testing.T) { + text := `sum func(a, b int) int { return a + b } +literal :: $32 +call :: $sum(1, 2) + 3 +block :: ${ + yield 4 +} +main func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + literal := module.exprs[module.globals[0].expr] + call_add := module.exprs[module.globals[1].expr] + call := module.exprs[call_add.left] + block := module.exprs[module.globals[2].expr] + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, literal.kind, ast.Expr_Kind.Comptime) + testing.expect_value(t, module.exprs[literal.left].kind, ast.Expr_Kind.Integer) + testing.expect_value(t, call_add.kind, ast.Expr_Kind.Add) + testing.expect_value(t, call.kind, ast.Expr_Kind.Comptime) + testing.expect_value(t, module.exprs[call.left].kind, ast.Expr_Kind.Call) + testing.expect_value(t, block.kind, ast.Expr_Kind.Comptime) + testing.expect_value(t, len(block.body), 1) + testing.expect_value(t, module.statements[block.body[0]].kind, ast.Stmt_Kind.Yield) +} + nested_expression_source :: proc(call: bool, depth: int) -> string { builder := strings.builder_make() defer strings.builder_destroy(&builder) @@ -2235,6 +2271,119 @@ main func() void { testing.expect(t, found_assign) } +@(test) +comptime_expression_forces_integer_evaluation :: proc(t: ^testing.T) { + text := `sum func(a, b int) int { + return a + b +} +max func(a, b int) int { + if a > b { + return a + } + return b +} +nested func(value int) int { + two :: 2 + return sum(value, two) +} +make_array func($N usize) [N]u8 { + data [N]u8 = undefined + return data +} +forced :: $sum(1, 2) +main func() i32 { + value i32 :: $sum(20, 22) + choice i32 :: $max(9, 3) + blocked i32 :: ${ + local :: 5 + yield sum(local, 6) + } + bytes [_]u8 :: make_array($nested(2)) + if forced != 3 { + return 1 + } + if value != 42 { + return 2 + } + if choice != 9 { + return 3 + } + if blocked != 11 { + return 4 + } + if bytes.len != 4 { + return 5 + } + return 0 +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 3")) +} + +@(test) +comptime_expression_diagnoses_unsupported_v1_evaluation :: proc(t: ^testing.T) { + text := `loop func() int { + while true { + return 1 + } + return 0 +} +missing func() int { + if true { + } +} +recurse func(value int) int { + return recurse(value) +} +main func() void { + runtime i32 = 1 + _ = $runtime + _ = $loop() + _ = $missing() + _ = $recurse(1) + _ = ${ + value :: 1 + } +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found := 0 + for diagnostic in diagnostics.items { + if strings.contains(diagnostic.message, "expression cannot be evaluated at comptime") { + found += 1 + } + } + testing.expect(t, found >= 5) +} + @(test) unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) { text := `broken func(value, value i8, nope void) void {} @@ -5615,6 +5764,16 @@ comptime_type_params_compile_and_run :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 0) } +@(test) +comptime_eval_compile_and_run :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-comptime-eval" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/comptime_eval", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + @(test) 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_eval/main.bro b/examples/programs/comptime_eval/main.bro new file mode 100644 index 0000000..d569630 --- /dev/null +++ b/examples/programs/comptime_eval/main.bro @@ -0,0 +1,49 @@ +sum func(a, b int) int { + return a + b +} + +max func(a, b int) int { + if a > b { + return a + } + return b +} + +nested func(value int) int { + two :: 2 + return sum(value, two) +} + +make_array func($N usize) [N]u8 { + data [N]u8 = undefined + return data +} + +forced :: $sum(1, 2) + +main func() i32 { + value i32 :: $sum(20, 22) + choice i32 :: $max(9, 3) + blocked i32 :: ${ + local :: 5 + yield sum(local, 6) + } + bytes [_]u8 :: make_array($nested(2)) + + if forced != 3 { + return 1 + } + if value != 42 { + return 2 + } + if choice != 9 { + return 3 + } + if blocked != 11 { + return 4 + } + if bytes.len != 4 { + return 5 + } + return 0 +}