feat: evaluate compile-time scalar procedure calls
This commit is contained in:
@@ -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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
`defer`/`errdefer` run through both paths; source errors are values, not diagnostic
|
||||||
traps or compiler infrastructure failures.
|
traps or compiler infrastructure failures.
|
||||||
cached generic specialization and type-producing procedures support explicit/inferred
|
cached generic specialization and type-producing procedures support explicit/inferred
|
||||||
compile-time arguments and generated record/array storage through both paths;
|
compile-time arguments and generated record/array storage through both paths.
|
||||||
general compile-time execution/reflection and generic library acceptance remain open.
|
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
|
the driver retains frontend storage and source provenance through
|
||||||
`@source/compilation`, with compilation-wide AST node identities.
|
`@source/compilation`, with compilation-wide AST node identities.
|
||||||
file commands retain single-file behavior; directory commands compile the
|
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`.
|
- [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.
|
- [ ] 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.
|
- [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`.
|
- [ ] 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.
|
- [ ] 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.
|
- [ ] 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.
|
||||||
|
|||||||
@@ -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)
|
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
|
@hide:file
|
||||||
add_generated_literal proc(
|
add_generated_literal proc(
|
||||||
builder @mut Builder, at Span, kind LiteralKind, bytes []u8,
|
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)
|
text :: try mem.alloc(u8, allocator, bytes.len)
|
||||||
errdefer mem.free(allocator, text)
|
errdefer mem.free(allocator, text)
|
||||||
memcopy!(text, bytes)
|
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
|
index :: builder.storage.generated.items.len
|
||||||
debug.assert(u64(index) < u64(maxval!(u32)))
|
debug.assert(u64(index) < u64(maxval!(u32)))
|
||||||
try arraylist.append(&builder.storage.generated, text)
|
try arraylist.append(&builder.storage.generated, text)
|
||||||
|
|||||||
@@ -79,9 +79,17 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
|
|||||||
bytes := try owned_text(
|
bytes := try owned_text(
|
||||||
`identity proc($T type, value T) T { return value }
|
`identity proc($T type, value T) T { return value }
|
||||||
`Box proc($T type) type { return struct { value T } }
|
`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 {
|
`main proc() i16 {
|
||||||
` box Box(i16) :: { value = identity(i16, 40) }
|
` 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]
|
||||||
`}
|
`}
|
||||||
,
|
,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ Code :: enum {
|
|||||||
cannot_infer_generic
|
cannot_infer_generic
|
||||||
specialization_cycle
|
specialization_cycle
|
||||||
specialization_limit
|
specialization_limit
|
||||||
|
comptime_execution_limit
|
||||||
}
|
}
|
||||||
|
|
||||||
Diagnostic :: struct {
|
Diagnostic :: struct {
|
||||||
@@ -243,5 +244,6 @@ messages :: $enummap.init(Code, []u8, {
|
|||||||
cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments",
|
cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments",
|
||||||
specialization_cycle = "compile-time specialization depends on its unfinished result",
|
specialization_cycle = "compile-time specialization depends on its unfinished result",
|
||||||
specialization_limit = "compile-time specialization exceeds its recursion or instance limit",
|
specialization_limit = "compile-time specialization exceeds its recursion or instance limit",
|
||||||
|
comptime_execution_limit = "compile-time execution exceeds its recursion or step limit",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,28 @@ run proc(
|
|||||||
allocator mem.Allocator,
|
allocator mem.Allocator,
|
||||||
module @ir.Module,
|
module @ir.Module,
|
||||||
diagnostic_store @mut diagnostics.Store,
|
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) {
|
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
entry ir.FunctionId := ir.FunctionId(0)
|
entry ir.FunctionId := ir.FunctionId(0)
|
||||||
if module.entry |function| {
|
if module.entry |function| {
|
||||||
@@ -157,6 +179,15 @@ run proc(
|
|||||||
function :: &module.functions.items[usize(frames.items[frame_index].function)]
|
function :: &module.functions.items[usize(frames.items[frame_index].function)]
|
||||||
block :: &function.blocks.items[usize(frames.items[frame_index].block)]
|
block :: &function.blocks.items[usize(frames.items[frame_index].block)]
|
||||||
instruction_index :: frames.items[frame_index].next_instruction
|
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 {
|
if instruction_index == block.instructions.items.len {
|
||||||
terminator :: block.terminator
|
terminator :: block.terminator
|
||||||
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
|
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
|
||||||
|
|||||||
+29
-2
@@ -72,6 +72,32 @@ lower_package proc(
|
|||||||
checked @typecheck.Result,
|
checked @typecheck.Result,
|
||||||
root_package ast.NodeId,
|
root_package ast.NodeId,
|
||||||
diagnostic_store @mut diagnostics.Store,
|
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) {
|
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
module := ir.init(allocator)
|
module := ir.init(allocator)
|
||||||
errdefer ir.deinit(&module)
|
errdefer ir.deinit(&module)
|
||||||
@@ -123,7 +149,8 @@ lower_package proc(
|
|||||||
.import, .invalid: {}
|
.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| {
|
for (0..ast.package_decl_count(tree)) |i| {
|
||||||
id :: ast.package_decl(tree, i)
|
id :: ast.package_decl(tree, i)
|
||||||
match ast.decl_kind(tree, id) {
|
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
|
@hide:file
|
||||||
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
span :: ast.node_span(state.tree, id)
|
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)
|
owned :: try mem.alloc(u8, state.module.allocator, text.len - 2)
|
||||||
transferred := false
|
transferred := false
|
||||||
defer if (!transferred) mem.free(state.module.allocator, owned)
|
defer if (!transferred) mem.free(state.module.allocator, owned)
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import "@std/hashmap"
|
|||||||
import "@source/ast"
|
import "@source/ast"
|
||||||
import "@source/diagnostics"
|
import "@source/diagnostics"
|
||||||
import "@source/strpool"
|
import "@source/strpool"
|
||||||
|
import "@source/lower"
|
||||||
|
import "@source/interpreter"
|
||||||
|
import "@source/ir"
|
||||||
|
|
||||||
sema :: import "@source/sema"
|
sema :: import "@source/sema"
|
||||||
|
typecheck :: import "@source/sema/check"
|
||||||
|
|
||||||
@hide:file no_node :: maxval!(ast.NodeId)
|
@hide:file no_node :: maxval!(ast.NodeId)
|
||||||
@hide:file no_index :: maxval!(usize)
|
@hide:file no_index :: maxval!(usize)
|
||||||
@@ -81,6 +86,8 @@ State :: struct {
|
|||||||
original_count usize
|
original_count usize
|
||||||
rewrites @mut Result
|
rewrites @mut Result
|
||||||
mapping []mut ast.NodeId
|
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.
|
#! elaborates templates into cached concrete declarations in the caller-owned arena.
|
||||||
@@ -117,6 +124,7 @@ elaborate proc(
|
|||||||
original_count = ast.node_count(&original),
|
original_count = ast.node_count(&original),
|
||||||
rewrites = &result,
|
rewrites = &result,
|
||||||
mapping = mapping,
|
mapping = mapping,
|
||||||
|
resolution = resolution,
|
||||||
}
|
}
|
||||||
defer arraylist.deinit(&state.bindings)
|
defer arraylist.deinit(&state.bindings)
|
||||||
defer arraylist.deinit(&state.declarations)
|
defer arraylist.deinit(&state.declarations)
|
||||||
@@ -268,9 +276,10 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
|
|||||||
position += 1
|
position += 1
|
||||||
try expand(state, argument)
|
try expand(state, argument)
|
||||||
if (view.comptime) {
|
if (view.comptime) {
|
||||||
|
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
||||||
arguments[i] = try evaluate(state, argument)
|
arguments[i] = try evaluate(state, argument)
|
||||||
if (arguments[i].kind == .invalid) {
|
if (arguments[i].kind == .invalid) {
|
||||||
try reject(state, call, .comptime_argument_required)
|
try reject_evaluation(state, call)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (is_type(state, view.type) != (arguments[i].kind == .type)) {
|
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)) {
|
if (is_type(state, view.return_type)) {
|
||||||
|
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
||||||
returned :: try return_type_value(state, view.body)
|
returned :: try return_type_value(state, view.body)
|
||||||
if (returned == no_node) {
|
if (returned == no_node) {
|
||||||
try reject(state, call, .comptime_argument_required)
|
try reject_evaluation(state, call)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result := returned
|
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
|
@hide:file
|
||||||
return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error {
|
return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error {
|
||||||
count :: ast.block_statement_count(&state.tree, block)
|
count :: ast.block_statement_count(&state.tree, block)
|
||||||
@@ -616,6 +756,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
.binary: return try evaluate_binary(state, id)
|
.binary: return try evaluate_binary(state, id)
|
||||||
|
.call: return try evaluate_call(state, id)
|
||||||
else: return Value{}
|
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 {
|
evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error {
|
||||||
view :: ast.binary_expr(&state.tree, id)
|
view :: ast.binary_expr(&state.tree, id)
|
||||||
left :: try evaluate(state, view.left)
|
left :: try evaluate(state, view.left)
|
||||||
|
if (left.kind == .invalid) return Value{}
|
||||||
if (left.kind == .boolean) {
|
if (left.kind == .boolean) {
|
||||||
if (view.operator == .logical_and and !left.boolean) return Value{ kind = .boolean, node = id, boolean = false }
|
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 }
|
if (view.operator == .logical_or and left.boolean) return Value{ kind = .boolean, node = id, boolean = true }
|
||||||
|
|||||||
+153
-2
@@ -11,6 +11,131 @@ HONEY = ROOT / "build/honey"
|
|||||||
|
|
||||||
# Explicit expectations guard against frontend bugs shared by both execution paths.
|
# Explicit expectations guard against frontend bugs shared by both execution paths.
|
||||||
CASES = {
|
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": (
|
"specialization_parameterless_type_producer_is_cached": (
|
||||||
"""
|
"""
|
||||||
Token proc() type { return struct { value i16 } }
|
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 {
|
first proc($T type, $N usize, value @Fixed(T, N)) T {
|
||||||
return value.values[0]
|
return value.values[0]
|
||||||
}
|
}
|
||||||
|
count proc() usize {
|
||||||
|
result := 0
|
||||||
|
while (result < 2) result = result + 1
|
||||||
|
return result
|
||||||
|
}
|
||||||
""").strip() + "\n")
|
""").strip() + "\n")
|
||||||
source = app / "main.hon"
|
source = app / "main.hon"
|
||||||
source.write_text(textwrap.dedent("""
|
source.write_text(textwrap.dedent("""
|
||||||
@@ -2579,7 +2709,7 @@ def check_specialization_packages(temporary, executable):
|
|||||||
main proc() i16 {
|
main proc() i16 {
|
||||||
imported ImportedBox :: { value = 40 }
|
imported ImportedBox :: { value = 40 }
|
||||||
copy lib.Box(i16) :: imported
|
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))
|
return copy.value + lib.identity(lib.first(&fixed))
|
||||||
}
|
}
|
||||||
""").strip() + "\n")
|
""").strip() + "\n")
|
||||||
@@ -2594,7 +2724,28 @@ def check_specialization_packages(temporary, executable):
|
|||||||
}
|
}
|
||||||
""").strip() + "\n")
|
""").strip() + "\n")
|
||||||
check(app, executable, (b"", b"incompatible value type"))
|
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):
|
def check_dependency_provenance(temporary, executable):
|
||||||
|
|||||||
Reference in New Issue
Block a user