feat: evaluate compile-time scalar procedure calls
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
`}
|
||||
,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+29
-2
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user