From ecd20675ea13c74532406c20bb297330162c5b15 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 26 Oct 2026 14:50:34 +0100 Subject: [PATCH] feat: evaluate compile-time scalar procedure calls --- README.md | 4 +- TODO.md | 9 +- source/ast/ast.hon | 27 +++++ source/compilation/compilation.test.hon | 10 +- source/diagnostics/diagnostics.hon | 2 + source/interpreter/interpreter.hon | 31 +++++ source/lower/lower.hon | 31 ++++- source/sema/specialize/specialize.hon | 146 +++++++++++++++++++++- tests/native.py | 155 +++++++++++++++++++++++- 9 files changed, 404 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9d8976d..c2b021a 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,9 @@ main proc() i16 { } ``` -the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures require compile-time parameters (or no parameters) and support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records. runtime values and mutable bindings cannot supply compile-time arguments. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. arbitrary compile-time procedure execution, reflection, `inline` iteration and full generic standard-library compilation remain outside this checkpoint. +the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures require compile-time parameters (or no parameters) and support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. + +ordinary procedures may compute integer, boolean and string compile-time arguments from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. each evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable global dependencies cannot supply compile-time inputs, and executed foreign calls remain interpreter traps. aggregate/type-valued execution, explicit `$()` expressions, reflection, `inline` expansion and existing generic-library acceptance remain outside this checkpoint. callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage. diff --git a/TODO.md b/TODO.md index f456002..eb27810 100644 --- a/TODO.md +++ b/TODO.md @@ -18,8 +18,10 @@ explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO `defer`/`errdefer` run through both paths; source errors are values, not diagnostic traps or compiler infrastructure failures. cached generic specialization and type-producing procedures support explicit/inferred -compile-time arguments and generated record/array storage through both paths; -general compile-time execution/reflection and generic library acceptance remain open. +compile-time arguments and generated record/array storage through both paths. +ordinary procedures can compute integer/boolean/string compile-time arguments using +mutable locals, loops and recursive calls; general aggregate/type-valued execution, +reflection and generic library acceptance remain open. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. file commands retain single-file behavior; directory commands compile the @@ -57,7 +59,8 @@ test discovery and FFI remain outside these completed slices. - [x] fallible results and cleanup: explicit `T ! E`, error-set composition, `try`/`catch`, `defer`/`errdefer`, error captures and matched error residuals through interpreter/native execution; infrastructure errors remain distinct from diagnostic traps. fallible `void` may complete on fallthrough; unhandled entry errors exit with status `1`. inferred `T!` signatures and yielding catch blocks remain with inference/value-control-flow work. examples: `source/main.hon`, `source/sema/check/check.hon`. - [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage. - [x] specialization checkpoint: explicit/inferred type and integer/boolean/string/callback/null value parameters, cached concrete runtime calls, nominal type-producing procedures, generated record/array storage and recursive data pointers through interpreter/native execution. preserve declared value types, source diagnostics and repeatable compilation; runtime values cannot supply compile-time arguments. - - [ ] remaining gate: general compile-time procedure execution, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates and formatting parsing may interleave with the next source-construct gate. + - [x] compile-time execution checkpoint: ordinary procedure calls with constant inputs and integer/boolean/string results reuse checking, typed IR and bounded interpreter execution. mutable scalar locals, range/while loops, break/continue, nested/generic/recursive calls, exact numeric types, runtime traps and dependency-file provenance survive argument erasure through interpreter/native execution. runtime parameters and mutable global dependencies are rejected; recompilation retains no prior execution state. + - [ ] remaining gate: general aggregate/type-valued compile-time execution, explicit compile-time expressions, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index dd119d4..a7b70c6 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -2042,6 +2042,25 @@ add_boolean proc(builder @mut Builder, at Span, value bool) NodeId ! mem.AllocEr return try add_generated_literal(builder, at, .bool, text) } +#! copies arbitrary bytes into owned quoted spelling, retaining `at` for diagnostics. +add_string proc(builder @mut Builder, at Span, bytes []u8) NodeId ! mem.AllocError { + if (bytes.len > divtrunc!(maxval!(usize) - 2, usize(4))) return .out_of_memory + allocator :: builder.storage.generated.allocator + text :: try mem.alloc(u8, allocator, bytes.len * 4 + 2) + errdefer mem.free(allocator, text) + text[0] = '"' + digits :: "0123456789abcdef" + for (bytes) |byte, index| { + start :: index * 4 + 1 + text[start] = '\\' + text[start + 1] = 'x' + text[start + 2] = digits[usize(divtrunc!(byte, u8(16)))] + text[start + 3] = digits[usize(rem!(byte, u8(16)))] + } + text[text.len - 1] = '"' + return try add_owned_literal(builder, at, .string, text) +} + @hide:file add_generated_literal proc( builder @mut Builder, at Span, kind LiteralKind, bytes []u8, @@ -2051,6 +2070,14 @@ add_generated_literal proc( text :: try mem.alloc(u8, allocator, bytes.len) errdefer mem.free(allocator, text) memcopy!(text, bytes) + return try add_owned_literal(builder, at, kind, text) +} + +@hide:file +add_owned_literal proc( + builder @mut Builder, at Span, kind LiteralKind, text []mut u8, +) NodeId ! mem.AllocError { + debug.assert(usize(at.source) < builder.storage.sources.items.len) index :: builder.storage.generated.items.len debug.assert(u64(index) < u64(maxval!(u32))) try arraylist.append(&builder.storage.generated, text) diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index c89e0bc..3f9be57 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -79,9 +79,17 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { bytes := try owned_text( `identity proc($T type, value T) T { return value } `Box proc($T type) type { return struct { value T } } + `count proc() usize { + ` result := 0 + ` while (result < 2) result = result + 1 + ` return result + `} + `Fixed proc($N usize) type { return struct { values [N]i16 } } `main proc() i16 { ` box Box(i16) :: { value = identity(i16, 40) } - ` return identity(box.value) + 2 + ` fixed Fixed(count()) :: { values = [box.value, 2] } + ` same Fixed(2) :: fixed + ` return identity(same.values[0]) + same.values[1] `} , ) diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 785e883..85417b8 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -111,6 +111,7 @@ Code :: enum { cannot_infer_generic specialization_cycle specialization_limit + comptime_execution_limit } Diagnostic :: struct { @@ -243,5 +244,6 @@ messages :: $enummap.init(Code, []u8, { cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments", specialization_cycle = "compile-time specialization depends on its unfinished result", specialization_limit = "compile-time specialization exceeds its recursion or instance limit", + comptime_execution_limit = "compile-time execution exceeds its recursion or step limit", }) diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 2dbce84..0732fd4 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -87,6 +87,28 @@ run proc( allocator mem.Allocator, module @ir.Module, diagnostic_store @mut diagnostics.Store, +) Outcome ! (mem.AllocError | diagnostics.CapacityError) { + return try execute(false, allocator, module, diagnostic_store, null) +} + +#! executes an isolated compile-time module with bounded work and stack depth. +#! ownership and diagnostic identities follow run. +run_evaluation proc( + allocator mem.Allocator, + module @ir.Module, + diagnostic_store @mut diagnostics.Store, +) Outcome ! (mem.AllocError | diagnostics.CapacityError) { + remaining := usize(100000) + return try execute(true, allocator, module, diagnostic_store, &remaining) +} + +@hide:file +execute proc( + $bounded bool, + allocator mem.Allocator, + module @ir.Module, + diagnostic_store @mut diagnostics.Store, + remaining ?@mut usize, ) Outcome ! (mem.AllocError | diagnostics.CapacityError) { entry ir.FunctionId := ir.FunctionId(0) if module.entry |function| { @@ -157,6 +179,15 @@ run proc( function :: &module.functions.items[usize(frames.items[frame_index].function)] block :: &function.blocks.items[usize(frames.items[frame_index].block)] instruction_index :: frames.items[frame_index].next_instruction + if $(bounded) { + budget :: remaining? + if (budget^ == 0 or frames.items.len > 128) { + span :: if (instruction_index < block.instructions.items.len) + block.instructions.items[instruction_index].span else block.terminator.span + return try runtime_trap(diagnostic_store, .comptime_execution_limit, span) + } + budget^ -= 1 + } if instruction_index == block.instructions.items.len { terminator :: block.terminator if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic } diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 9be11c4..c7ff6c3 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -72,6 +72,32 @@ lower_package proc( checked @typecheck.Result, root_package ast.NodeId, diagnostic_store @mut diagnostics.Store, +) ir.Module ! (mem.AllocError | diagnostics.CapacityError) { + return try lower_module(allocator, tree, resolution, checked, root_package, null, diagnostic_store) +} + +#! lowers an isolated compile-time call wrapper without applying program entry-name rules. +#! the caller supplies a checked parameterless function and owns the returned IR. +lower_evaluation proc( + allocator mem.Allocator, + tree @ast.Tree, + resolution @sema.Resolution, + checked @typecheck.Result, + entry ast.NodeId, + diagnostic_store @mut diagnostics.Store, +) ir.Module ! (mem.AllocError | diagnostics.CapacityError) { + return try lower_module(allocator, tree, resolution, checked, tree.root, entry, diagnostic_store) +} + +@hide:file +lower_module proc( + allocator mem.Allocator, + tree @ast.Tree, + resolution @sema.Resolution, + checked @typecheck.Result, + root_package ast.NodeId, + entry ?ast.NodeId, + diagnostic_store @mut diagnostics.Store, ) ir.Module ! (mem.AllocError | diagnostics.CapacityError) { module := ir.init(allocator) errdefer ir.deinit(&module) @@ -123,7 +149,8 @@ lower_package proc( .import, .invalid: {} } } - try find_entry(&state, root_package) + if (entry) |function| module.entry = ir.FunctionId(storage[usize(function)].index) + else try find_entry(&state, root_package) for (0..ast.package_decl_count(tree)) |i| { id :: ast.package_decl(tree, i) match ast.decl_kind(tree, id) { @@ -2136,7 +2163,7 @@ lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError @hide:file lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { span :: ast.node_span(state.tree, id) - text :: ast.source_text(state.tree, span) + text :: ast.literal_text(state.tree, id) owned :: try mem.alloc(u8, state.module.allocator, text.len - 2) transferred := false defer if (!transferred) mem.free(state.module.allocator, owned) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 18be0ab..3eb154c 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -4,7 +4,12 @@ import "@std/hashmap" import "@source/ast" import "@source/diagnostics" import "@source/strpool" +import "@source/lower" +import "@source/interpreter" +import "@source/ir" + sema :: import "@source/sema" +typecheck :: import "@source/sema/check" @hide:file no_node :: maxval!(ast.NodeId) @hide:file no_index :: maxval!(usize) @@ -81,6 +86,8 @@ State :: struct { original_count usize rewrites @mut Result mapping []mut ast.NodeId + resolution @mut sema.Resolution + evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic } #! elaborates templates into cached concrete declarations in the caller-owned arena. @@ -117,6 +124,7 @@ elaborate proc( original_count = ast.node_count(&original), rewrites = &result, mapping = mapping, + resolution = resolution, } defer arraylist.deinit(&state.bindings) defer arraylist.deinit(&state.declarations) @@ -268,9 +276,10 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { position += 1 try expand(state, argument) if (view.comptime) { + state.evaluation_diagnostic = diagnostics.no_diagnostic arguments[i] = try evaluate(state, argument) if (arguments[i].kind == .invalid) { - try reject(state, call, .comptime_argument_required) + try reject_evaluation(state, call) return } if (is_type(state, view.type) != (arguments[i].kind == .type)) { @@ -458,9 +467,10 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { } } if (is_type(state, view.return_type)) { + state.evaluation_diagnostic = diagnostics.no_diagnostic returned :: try return_type_value(state, view.body) if (returned == no_node) { - try reject(state, call, .comptime_argument_required) + try reject_evaluation(state, call) return } result := returned @@ -495,6 +505,136 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { } } +@hide:file +reject_evaluation proc(state @mut State, call ast.NodeId) void ! Error { + diagnostic :: state.evaluation_diagnostic + if (diagnostic == diagnostics.no_diagnostic) { + try reject(state, call, .comptime_argument_required) + return + } + invalid :: try ast.add_invalid_expr(state.builder, ast.node_span(&state.tree, call), diagnostic) + try replace(state, call, invalid) + try refresh(state) + state.bindings.items[usize(call)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } } +} + +@hide:file +evaluation_dependencies proc( + state @mut State, id ast.NodeId, flags []mut u8, declarations @mut arraylist.ArrayList(ast.NodeId), +) bool ! Error { + if ((flags[usize(id)] & 4) != 0) return true + flags[usize(id)] = flags[usize(id)] | 4 + resolved :: binding(state, id) + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + if ((flags[usize(target)] & 1) != 0 and (flags[usize(target)] & 2) == 0) { + if (ast.node_class(&state.tree, target) == .statement + and ast.statement_decl(&state.tree, target).mutability == .variable) return false + flags[usize(target)] = flags[usize(target)] | 2 + try arraylist.append(declarations, target) + } + } + for (0..ast.child_count(&state.tree, id)) |i| { + if (!try evaluation_dependencies(state, ast.child(&state.tree, id, i), flags, declarations)) return false + } + return true +} + +@hide:file +evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error { + callee :: try evaluate(state, ast.call_expr(&state.tree, call).callee) + if (callee.kind != .function and callee.kind != .type) return Value{} + result_type := callee.node + if (callee.kind == .function) { + resolved :: binding(state, callee.node) + if (resolved.kind != .declaration) return Value{} + result_type = ast.function_decl(&state.tree, resolved.data.declaration).return_type + } + arguments := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&arguments) + for (0..ast.call_arg_count(&state.tree, call)) |i| { + value :: try evaluate(state, ast.call_arg(&state.tree, call, i)) + if (value.kind == .invalid) return Value{} + concrete := try materialize(state, value) + if (value.annotation != no_node) { + concrete.node = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, value.node), + value.annotation, concrete.node) + try refresh(state) + } + try arraylist.append(&arguments, concrete.node) + } + # Reuse checking, typed IR and the interpreter: compile-time arithmetic and local + # inference must have exactly the same semantics as an ordinary runtime call. + span :: ast.node_span(&state.tree, call) + invocation :: try ast.add_call(state.builder, span, callee.node, arguments.items) + returned :: try ast.add_return_stmt(state.builder, span, invocation) + statements [1]ast.NodeId :: [returned] + body :: try ast.add_block(state.builder, span, statements[..]) + wrapper :: try ast.add_function_decl(state.builder, span, span, strpool.no_str, result_type, &[], body) + try refresh(state) + declarations := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&declarations) + try arraylist.append(&declarations, wrapper) + flags := arraylist.init(u8, state.allocator) + defer arraylist.deinit(&flags) + index := usize(0) + registered := usize(0) + while (index < declarations.items.len) { + declaration :: declarations.items[index] + try expand(state, declaration) + while (flags.items.len < ast.node_count(&state.tree)) try arraylist.append(&flags, u8(0)) + while (registered < state.declarations.items.len) { + target :: state.declarations.items[registered] + flags.items[usize(target)] = flags.items[usize(target)] | 1 + registered += 1 + } + flags.items[usize(declaration)] = flags.items[usize(declaration)] | 2 + if (!try evaluation_dependencies(state, declaration, flags.items, &declarations)) return Value{} + index += 1 + } + root :: try ast.add_package(state.builder, span, declarations.items) + try refresh(state) + bindings :: try mem.alloc(sema.Binding, state.allocator, state.bindings.items.len) + memcopy!(bindings, state.bindings.items) + sema.replace_bindings(state.resolution, bindings) + tree :: ast.view(state.builder, root) + diagnostic_store := diagnostics.init(state.allocator) + defer diagnostics.deinit(&diagnostic_store) + # Resolution's existing diagnostic IDs remain valid in this temporary store. + for (state.diagnostics.list.items) |diagnostic| try arraylist.append(&diagnostic_store.list, diagnostic) + original_diagnostics :: diagnostics.count(&diagnostic_store) + checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store) + defer typecheck.deinit(&checked) + if (diagnostics.count(&diagnostic_store) != original_diagnostics) return Value{} + module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked, wrapper, &diagnostic_store) + defer ir.deinit(&module) + outcome := try interpreter.run_evaluation(state.allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + if (outcome.trap) |trap| { + if (usize(trap) < original_diagnostics) state.evaluation_diagnostic = trap + else { + diagnostic :: diagnostics.get(&diagnostic_store, trap)? + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span) + } + return Value{} + } + value :: outcome.value + if (ir.integer_bits(value.type) != 0) { + negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0 + magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned + return Value{ kind = .integer, node = call, magnitude = magnitude, negative = negative, annotation = result_type } + } + if (value.type == .bool) return Value{ + kind = .boolean, node = call, boolean = value.data.boolean, annotation = result_type, + } + if (value.type == .string) { + literal :: try ast.add_string(state.builder, span, ir.string_bytes(&module, value.data.string)) + try refresh(state) + return Value{ kind = .string, node = literal, annotation = result_type } + } + return Value{} +} + @hide:file return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error { count :: ast.block_statement_count(&state.tree, block) @@ -616,6 +756,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { return value } .binary: return try evaluate_binary(state, id) + .call: return try evaluate_call(state, id) else: return Value{} } } @@ -624,6 +765,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { view :: ast.binary_expr(&state.tree, id) left :: try evaluate(state, view.left) + if (left.kind == .invalid) return Value{} if (left.kind == .boolean) { if (view.operator == .logical_and and !left.boolean) return Value{ kind = .boolean, node = id, boolean = false } if (view.operator == .logical_or and left.boolean) return Value{ kind = .boolean, node = id, boolean = true } diff --git a/tests/native.py b/tests/native.py index e26a42d..1858637 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,131 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "comptime_calls_share_runtime_inference_loops_and_recursion": ( + """ + length proc() usize { + result := 0 + while (result < 2) result = result + 1 + return result + } + factorial proc(count usize) usize { + if (count == 0) return 1 + return count * factorial(count - 1) + } + sum proc(count usize) usize { + result := 0 + for (0..count) |index| { + if (index == 1) continue + if (index == 4) break + result = result + index + } + return result + } + identity proc($T type, value T) T { return value } + Fixed proc($N usize) type { return struct { values [N]i16 } } + main proc() i16 { + first Fixed(identity(usize, length())) :: { values = [40, 2] } + same Fixed(factorial(3) - sum(6) + 1) :: first + return same.values[0] + same.values[1] + } + """, b"42\n", None, + ), + "comptime_call_string_result_owns_escaped_bytes": ( + r""" + choose proc(take bool, text string) string { + if (take) return text + return "unused" + } + offset proc($text string, value i16) i16 { return value + i16(text.len) } + main proc() i16 { return offset(choose(true, "a\"\\\x00é"), 36) } + """, b"42\n", None, + ), + "comptime_calls_preserve_checked_narrow_intermediates": ( + """ + overflow proc(seed i8) i8 { + result := seed + result = result + 1 + return result - 1 + } + answer proc($N i8) i16 { return 42 } + main proc() i16 { return answer(overflow(127)) } + """, b"", b"integer arithmetic overflow", + ), + "comptime_calls_preserve_unsigned_and_signed_boundaries": ( + """ + unsigned proc(value u64) u64 { return value } + signed proc(value i64) i64 { return value } + answer proc($high u64, $low i64) i16 { + if (high == 18446744073709551615 and low == -9223372036854775808) return 42 + return 0 + } + main proc() i16 { + return answer(unsigned(18446744073709551615), signed(-9223372036854775808)) + } + """, b"42\n", None, + ), + "comptime_call_cannot_promote_runtime_parameter": ( + """ + identity proc(value usize) usize { return value } + answer proc($N usize) i16 { return 42 } + runtime proc(value usize) i16 { return answer(identity(value)) } + main proc() i16 { return runtime(2) } + """, b"", b"generic argument must be a compile-time value", + ), + "comptime_call_cannot_read_mutable_global": ( + """ + state usize := 2 + read proc() usize { return state } + answer proc($N usize) i16 { return 42 } + main proc() i16 { return answer(read()) } + """, b"", b"generic argument must be a compile-time value", + ), + "comptime_call_cannot_mutate_runtime_storage": ( + """ + state usize := 0 + write proc() usize { + state = 2 + return state + } + answer proc($N usize) i16 { return 42 } + main proc() i16 { return answer(write()) } + """, b"", b"generic argument must be a compile-time value", + ), + "comptime_call_keeps_uninitialized_local_traps": ( + """ + premature proc() usize { + before :: after + after :: 2 + return before + } + answer proc($N usize) i16 { return 42 } + main proc() i16 { return answer(premature()) } + """, b"", b"local value was read before initialization", + ), + "comptime_call_bounds_empty_loops": ( + """ + forever proc() usize { + while true {} + return 2 + } + answer proc($N usize) i16 { return 42 } + main proc() i16 { return answer(forever()) } + """, b"", b"compile-time execution exceeds", + ), + "comptime_call_bounds_recursive_frames": ( + """ + forever proc(value usize) usize { return forever(value) } + answer proc($N usize) i16 { return 42 } + main proc() i16 { return answer(forever(2)) } + """, b"", b"compile-time execution exceeds", + ), + "comptime_call_foreign_execution_remains_a_source_trap": ( + """ + getpid c_func() c_int + answer proc($N c_int) i16 { return 42 } + main proc() i16 { return answer(getpid()) } + """, b"", b"foreign function calls are unsupported", + ), "specialization_parameterless_type_producer_is_cached": ( """ Token proc() type { return struct { value i16 } } @@ -2570,6 +2695,11 @@ def check_specialization_packages(temporary, executable): first proc($T type, $N usize, value @Fixed(T, N)) T { return value.values[0] } + count proc() usize { + result := 0 + while (result < 2) result = result + 1 + return result + } """).strip() + "\n") source = app / "main.hon" source.write_text(textwrap.dedent(""" @@ -2579,7 +2709,7 @@ def check_specialization_packages(temporary, executable): main proc() i16 { imported ImportedBox :: { value = 40 } copy lib.Box(i16) :: imported - fixed lib.Fixed(i16, 2) :: { values = [2, 0] } + fixed lib.Fixed(i16, lib.count()) :: { values = [2, 0] } return copy.value + lib.identity(lib.first(&fixed)) } """).strip() + "\n") @@ -2594,7 +2724,28 @@ def check_specialization_packages(temporary, executable): } """).strip() + "\n") check(app, executable, (b"", b"incompatible value type")) - return 2 + sizing = library / "sizing.hon" + sizing.write_text(textwrap.dedent(""" + size proc() usize { + high usize :: 18446744073709551615 + return high + 1 + } + """).strip() + "\n") + source.write_text(textwrap.dedent(""" + lib :: import "../library" + main proc() i16 { + if (false) { + fixed lib.Fixed(i16, lib.size()) :: { values = [40, 2] } + return fixed.values[0] + } + return 42 + } + """).strip() + "\n") + check(app, executable, (b"42\n", b"integer arithmetic overflow")) + interpreted = run(str(HONEY), "run", str(app)) + assert interpreted.stderr.startswith(f"{sizing.resolve()}:3:12: ".encode()), outcome(interpreted) + assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) + return 3 def check_dependency_provenance(temporary, executable):