Files
honey/source/sema/specialize/specialize.hon
T
hl-valdemar cc39c96fc0 feat: materialize address-free compile-time untagged unions
Snapshot full union storage rather than choosing an overlapping field. Preserve structural union source syntax independently of nominal identity so direct literal arguments materialize and share normalized specialization keys. Reject uninitialized bytes and address-bearing member layouts.
2026-10-30 16:35:11 +01:00

5899 lines
279 KiB
Plaintext

import "@std/mem"
import "@std/arraylist"
import "@std/hashmap"
import "@std/enummap"
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)
@hide:file
Error :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError
@hide:file
ValueKind :: enum {
invalid
type
integer
boolean
string
function
null
real
aggregate
}
@hide:file
Value :: struct {
kind ValueKind = .invalid
node ast.NodeId = no_node
magnitude u64 = 0
negative bool = false
boolean bool = false
annotation ast.NodeId = no_node
}
@hide:file
Instance :: struct {
function ast.NodeId
arguments []mut Value
declaration ast.NodeId = no_node
type ast.NodeId = no_node
next usize = no_index
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
signature ast.NodeId = no_node
value ast.NodeId = no_node
}
@hide:file
Rewrite :: struct { target ast.NodeId, saved ast.NodeId }
#! owns an undo log and retained checking obligations; deinit releases them with their retained allocator.
#! borrows the caller's AST builder; call deinit before finishing or deinitializing that builder.
#! generated nodes remain arena-owned and are released with the AST.
Result :: struct {
builder @mut ast.Builder
changes arraylist.ArrayList(Rewrite)
constraints arraylist.ArrayList(ast.NodeId)
}
#! restores rewritten source nodes in the borrowed builder and releases the undo log and checking obligations.
#! does not remove generated arena nodes or restore resolution bindings.
deinit proc(result @mut Result) void {
index := result.changes.items.len
while (index > 0) {
index -= 1
change :: result.changes.items[index]
ast.replace_node_from_source(result.builder, change.target, change.saved)
}
arraylist.deinit(&result.changes)
arraylist.deinit(&result.constraints)
}
@hide:file
State :: struct {
allocator mem.Allocator
builder @mut ast.Builder
tree ast.Tree
strings @mut strpool.StringPool
bindings arraylist.ArrayList(sema.Binding)
declarations arraylist.ArrayList(ast.NodeId)
instances arraylist.ArrayList(Instance)
cache hashmap.AutoHashMap(usize, usize)
expanded arraylist.ArrayList(u8)
capture_types hashmap.AutoHashMap(ast.NodeId, ast.NodeId)
diagnostics @mut diagnostics.Store
depth usize = 0
original_count usize
rewrites @mut Result
mapping []mut ast.NodeId
resolution @mut sema.Resolution
evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
type_execution_depth usize = 0
type_execution_remaining usize = 0
value_expansion_depth usize = 0
return_type ast.NodeId = no_node
yield_type ast.NodeId = no_node
}
#! elaborates templates into cached concrete declarations in the caller-owned `builder` arena.
#! extends `resolution` to cover generated nodes; no template reaches checking or lowering.
#! `allocator` supplies returned logs, temporary storage released on every exit, and replacement resolution bindings.
#! use the allocator retained by `resolution`; the returned Result borrows `builder` until the caller calls deinit.
#! on failure, restores rewritten source nodes and releases logs; generated nodes remain arena-owned.
elaborate proc(
allocator mem.Allocator,
builder @mut ast.Builder,
root ast.NodeId,
resolution @mut sema.Resolution,
strings @mut strpool.StringPool,
diagnostic_store @mut diagnostics.Store,
) Result ! Error {
result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) }
errdefer deinit(&result)
# explicit evaluation is useful even in packages without generic declarations.
original :: ast.view(builder, root)
needed := false
for (0..ast.node_count(&original)) |i| {
id :: ast.NodeId(i)
class :: ast.node_class(&original, id)
if (class == .expression
and (ast.expr_kind(&original, id) == .comptime_expr or ast.expr_kind(&original, id) == .block_expr))
needed = true
if (class == .expression and ast.expr_kind(&original, id) == .binary
and ast.binary_expr(&original, id).operator == .bit_or) needed = true
if (class == .statement and ast.stmt_kind(&original, id) == .for_stmt
and ast.for_stmt(&original, id).is_inline) needed = true
if (class == .match_arm and ast.match_arm_view(&original, id).is_inline) needed = true
if (class == .expression and ast.expr_kind(&original, id) == .builtin_call) {
name :: ast.builtin_call(&original, id).name
text :: ast.source_text(&original, ast.node_span(&original, name))
if (mem.eql(text, "field") or mem.eql(text, "struct_type") or mem.eql(text, "typeinfo")
or mem.eql(text, "compile_error")) needed = true
}
if (class == .function and ast.function_generic(&original, id)) needed = true
if (class == .function) {
if (type_storage_annotation(&original, resolution.storage.bindings,
ast.function_decl(&original, id).return_type, 0)) needed = true
}
if (class == .statement and ast.stmt_kind(&original, id) == .declaration
and type_storage_annotation(&original, resolution.storage.bindings,
ast.statement_decl(&original, id).initializer, 0)) needed = true
if (needed) break
}
if (!needed) return result
mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original))
defer mem.free(allocator, mapping)
state := State{
allocator = allocator, builder = builder, tree = ast.view(builder, root), strings = strings,
bindings = arraylist.init(allocator), declarations = arraylist.init(allocator),
instances = arraylist.init(allocator), cache = hashmap.init(allocator),
capture_types = hashmap.init(allocator),
expanded = arraylist.init(allocator), diagnostics = diagnostic_store,
original_count = ast.node_count(&original),
rewrites = &result,
mapping = mapping,
resolution = resolution,
}
defer arraylist.deinit(&state.bindings)
defer arraylist.deinit(&state.declarations)
defer arraylist.deinit(&state.expanded)
defer hashmap.deinit(&state.cache)
defer hashmap.deinit(&state.capture_types)
defer {
for (state.instances.items) |instance| mem.free(allocator, instance.arguments)
arraylist.deinit(&state.instances)
}
count :: ast.node_count(&state.tree)
for (0..count) |i| try arraylist.append(&state.bindings, sema.binding(resolution, ast.NodeId(i)))
for (0..ast.package_decl_count(&state.tree)) |i| {
id :: ast.package_decl(&state.tree, i)
if (ast.decl_kind(&state.tree, id) == .function and template(&state, id)) continue
try arraylist.append(&state.declarations, id)
}
index := 0
while (index < state.declarations.items.len) {
try expand(&state, state.declarations.items[index])
index += 1
}
# metadata definitions have no runtime layout. retain their AST and bindings:
# surviving runtime annotations still resolve the definition and are rejected.
retained := usize(0)
index = 0
while (index < state.declarations.items.len) {
declaration :: state.declarations.items[index]
index += 1
if (ast.node_class(&state.tree, declaration) == .statement
and ast.stmt_kind(&state.tree, declaration) == .declaration) {
view :: ast.statement_decl(&state.tree, declaration)
if (view.mutability == .constant and type_record_storage(&state, view.initializer, 0)) {
state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
if (try type_record_check(&state, view.initializer)) continue
try reject_evaluation(&state, view.initializer)
}
}
state.declarations.items[retained] = declaration
retained += 1
}
state.declarations.items = state.declarations.items[..retained]
try remember(&state, root)
try ast.set_package_declarations(builder, root, state.declarations.items)
try refresh(&state)
owned :: try mem.alloc(sema.Binding, allocator, state.bindings.items.len)
memcopy!(owned, state.bindings.items)
sema.replace_bindings(resolution, owned)
return result
}
@hide:file
refresh proc(state @mut State) void ! mem.AllocError {
state.tree = ast.view(state.builder, state.tree.root)
count :: ast.node_count(&state.tree)
while (state.bindings.items.len < count) try arraylist.append(&state.bindings, sema.Binding{})
while (state.expanded.items.len < count) try arraylist.append(&state.expanded, u8(0))
}
@hide:file
binding proc(state @State, id ast.NodeId) sema.Binding {
if (usize(id) >= state.bindings.items.len) return sema.Binding{}
return state.bindings.items[usize(id)]
}
@hide:file
reject proc(state @mut State, id ast.NodeId, code diagnostics.Code) void ! Error {
span :: ast.node_span(&state.tree, id)
diagnostic :: try diagnostics.add(state.diagnostics, code, span)
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
try replace(state, id, invalid)
try refresh(state)
state.bindings.items[usize(id)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } }
}
@hide:file
expand_compile_error proc(state @mut State, id ast.NodeId) void ! Error {
if (ast.call_arg_count(&state.tree, id) != 1) {
try reject(state, id, .invalid_compile_error)
return
}
state.evaluation_diagnostic = diagnostics.no_diagnostic
value :: try evaluate(state, ast.call_arg(&state.tree, id, 0))
if (value.kind != .string) {
if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id)
else try reject(state, id, .invalid_compile_error)
return
}
text :: ast.literal_text(&state.tree, value.node)
bytes := arraylist.init(u8, state.allocator)
defer arraylist.deinit(&bytes)
cursor := usize(1)
while (cursor < text.len - 1) {
byte :: ast.string_next(text, &cursor) catch {
try reject(state, id, .invalid_escape)
return
}
try arraylist.append(&bytes, byte)
}
state.evaluation_diagnostic = try diagnostics.add_message(state.diagnostics,
.compile_error, ast.node_span(&state.tree, id), bytes.items)
try reject_evaluation(state, id)
}
@hide:file
expand proc(state @mut State, id ast.NodeId) void ! Error {
try expand_expected(state, id, no_node)
}
@hide:file
expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
try refresh(state)
if (state.expanded.items[usize(id)] != 0) return
state.expanded.items[usize(id)] = 1
class :: ast.node_class(&state.tree, id)
saved_return :: state.return_type
saved_yield :: state.yield_type
defer {
state.return_type = saved_return
state.yield_type = saved_yield
}
if (class == .function) {
state.return_type = ast.function_decl(&state.tree, id).return_type
state.yield_type = no_node
}
if (class == .statement) {
match ast.stmt_kind(&state.tree, id) {
.declaration: {
view :: ast.statement_decl(&state.tree, id)
annotation := no_node
if (view.type) |type| {
try expand(state, type)
annotation = type
}
try expand_expected(state, view.initializer, annotation)
return
}
.assignment_stmt: {
view :: ast.assignment_stmt(&state.tree, id)
try expand(state, view.target)
try expand_expected(state, view.value, try expression_type(state, view.target, 0))
return
}
.return_stmt, .yield_stmt: {
yielding :: ast.stmt_kind(&state.tree, id) == .yield_stmt
value :: if (yielding) ast.yield_stmt(&state.tree, id).value else ast.return_stmt(&state.tree, id).value
annotation :: if (yielding) state.yield_type else state.return_type
if (value) |operand| try expand_expected(state, operand, annotation)
return
}
else: {}
}
}
if (class == .field) {
view :: ast.field(&state.tree, id)
annotation := no_node
if (view.type) |type| {
try expand(state, type)
annotation = type
}
if (view.value) |value| try expand_expected(state, value, annotation)
return
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .block_expr) {
body :: ast.block_expr(&state.tree, id)
annotation :: if (expected != no_node) expected else try value_block_type(state, body, 0)
if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) {
selected :: try evaluate_type_value_block(state, id, annotation)
if (selected == no_node) {
try reject_evaluation(state, id)
return
}
try replace(state, id, selected)
try refresh(state)
state.bindings.items[usize(id)] = binding(state, selected)
return
}
state.value_expansion_depth += 1
defer state.value_expansion_depth -= 1
state.yield_type = annotation
try expand(state, body)
return
}
if (class == .block and state.value_expansion_depth > 0) {
statements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&statements)
count :: ast.block_statement_count(&state.tree, id)
for (0..count) |i| {
statement :: ast.block_statement(&state.tree, id, i)
try expand(state, statement)
try arraylist.append(&statements, statement)
if (value_block_terminal(state, statement)) break
}
if (statements.items.len != count) {
replacement :: try ast.add_block(state.builder, ast.node_span(&state.tree, id), statements.items)
try replace(state, id, replacement)
try refresh(state)
}
return
}
if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt
and ast.for_stmt(&state.tree, id).is_inline) {
try expand_inline_for(state, id)
return
}
if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt) {
loop :: ast.for_stmt(&state.tree, id)
try expand(state, loop.iterable)
if (loop.index) |capture| {
annotation :: try builtin(state, id, .usize)
hashmap.put(&state.capture_types, capture, annotation) catch |err| {
match err {
.key_exists: {}
else: return err
}
}
}
if (!loop.pointer_capture and loop.capture != null) {
iterable :: try expression_type(state, loop.iterable, 0)
element := no_node
if (ast.expr_kind(&state.tree, loop.iterable) == .range) {
element = try expression_type(state, ast.range_expr(&state.tree, loop.iterable).start, 0)
} else if (iterable != no_node) {
kind :: ast.expr_kind(&state.tree, iterable)
if (kind == .array_type) element = ast.array_type(&state.tree, iterable).element
if (kind == .slice_type) element = ast.slice_type(&state.tree, iterable).element
if (kind == .pointer_type) {
array :: try canonical_type(state, ast.pointer_type(&state.tree, iterable).element)
if (array != no_node and ast.expr_kind(&state.tree, array) == .array_type)
element = ast.array_type(&state.tree, array).element
}
if (binding(state, iterable).kind == .builtin
and binding(state, iterable).data.builtin == .string)
element = try builtin(state, id, .u8)
}
if (element != no_node) {
hashmap.put(&state.capture_types, loop.capture?, element) catch |err| {
match err {
.key_exists: {}
else: return err
}
}
}
}
}
if (class == .statement
and (ast.stmt_kind(&state.tree, id) == .if_stmt or ast.stmt_kind(&state.tree, id) == .while_stmt)) {
looping :: ast.stmt_kind(&state.tree, id) == .while_stmt
capture :: if (looping) ast.while_stmt(&state.tree, id).capture else ast.if_stmt(&state.tree, id).capture
condition :: if (looping) ast.while_stmt(&state.tree, id).condition else ast.if_stmt(&state.tree, id).condition
if (capture) |name| {
annotation :: try expression_type(state, condition, 0)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) {
child :: ast.type_operand(&state.tree, annotation)
hashmap.put(&state.capture_types, name, child) catch |err| {
match err {
.key_exists: {}
else: return err
}
}
}
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .catch_expr) {
expression :: ast.catch_expr(&state.tree, id)
try expand_expected(state, expression.operand, expected)
if (expression.capture) |capture| {
annotation :: try expression_type(state, expression.operand, 0)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) {
hashmap.put(&state.capture_types, capture, ast.binary_expr(&state.tree, annotation).right) catch |err| {
match err {
.key_exists: {}
else: return err
}
}
}
}
annotation :: if (expected != no_node) expected else try expression_type(state, id, 0)
state.yield_type = annotation
try expand_expected(state, expression.handler, annotation)
return
}
if (class == .statement and ast.stmt_kind(&state.tree, id) == .expression) {
expression :: ast.statement_expr(&state.tree, id)
if (
ast.expr_kind(&state.tree, expression) == .match_expr
and try specialize_match(state, expression, id, no_node)
) return
}
if (
class == .expression and ast.expr_kind(&state.tree, id) == .match_expr
and try specialize_match(state, id, no_node, expected)
) return
if (class == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) {
conditional :: ast.if_stmt(&state.tree, id)
explicit :: ast.expr_kind(&state.tree, conditional.condition) == .comptime_expr
if (conditional.capture == null and (explicit or usize(id) >= state.original_count)) {
condition :: if (explicit) ast.comptime_operand(&state.tree, conditional.condition) else conditional.condition
try expand(state, condition)
if (explicit or branch_constant(state, condition, 0)) {
state.evaluation_diagnostic = diagnostics.no_diagnostic
boolean :: try builtin(state, condition, .bool)
value :: try evaluate_ir(state, condition, boolean)
if (explicit and value.kind == .invalid and state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.comptime_argument_required, ast.node_span(&state.tree, condition))
if (state.evaluation_diagnostic != diagnostics.no_diagnostic) {
invalid :: try ast.add_invalid_stmt(state.builder, ast.node_span(&state.tree, id),
state.evaluation_diagnostic)
try replace(state, id, invalid)
try refresh(state)
return
}
if (value.kind == .boolean) {
branch := conditional.then_block
if (!value.boolean) {
if (conditional.else_block) |other| branch = other
else branch = try ast.add_block(state.builder, ast.node_span(&state.tree, id), &[])
}
replacement :: try ast.add_block_stmt(state.builder, branch)
try replace(state, id, replacement)
try refresh(state)
try expand(state, branch)
return
}
}
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .comptime_expr) {
operand :: ast.comptime_operand(&state.tree, id)
try expand_expected(state, operand, expected)
state.evaluation_diagnostic = diagnostics.no_diagnostic
value :: try evaluate(state, operand)
if (value.annotation == no_node or !type_record_storage(state, value.annotation, 0))
try arraylist.append(&state.rewrites.constraints, operand)
if (value.kind == .invalid) {
try reject_evaluation(state, id)
return
}
concrete :: try materialize(state, value)
replacement := concrete.node
if (value.annotation != no_node and value.kind != .type) {
replacement = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id),
value.annotation, replacement)
try refresh(state)
}
try replace(state, id, replacement)
try refresh(state)
return
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .builtin_call) {
name :: ast.builtin_call(&state.tree, id).name
if (
mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "some")
and expected != no_node and ast.call_arg_count(&state.tree, id) == 1
) {
annotation := try canonical_type(state, expected)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type)
annotation = try canonical_type(state, ast.binary_expr(&state.tree, annotation).left)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) {
try expand_expected(state, ast.call_arg(&state.tree, id, 0), ast.type_operand(&state.tree, annotation))
return
}
}
if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "compile_error")) {
try expand_compile_error(state, id)
return
}
if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) {
try expand_field(state, id)
return
}
if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "struct_type")) {
try expand_struct_type(state, id)
return
}
if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "typeinfo")) {
try expand_typeinfo(state, id)
return
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .call) {
callee :: ast.call_expr(&state.tree, id).callee
try expand(state, callee)
resolved :: binding(state, callee)
if (
resolved.kind == .declaration
and ast.node_class(&state.tree, resolved.data.declaration) == .function
and template(state, resolved.data.declaration)
) {
try specialize_call(state, id, resolved.data.declaration, expected)
return
}
signature := no_node
if (resolved.kind == .declaration and ast.node_class(&state.tree, resolved.data.declaration) == .function)
signature = resolved.data.declaration
else signature = try expression_type(state, callee, 0)
if (
signature != no_node
and (ast.node_class(&state.tree, signature) == .function
or ast.expr_kind(&state.tree, signature) == .function_type)
and ast.function_param_count(&state.tree, signature) == ast.call_arg_count(&state.tree, id)
) {
for (0..ast.call_arg_count(&state.tree, id)) |i| {
annotation :: ast.param(&state.tree, ast.function_param(&state.tree, signature, i)).type
try expand_expected(state, ast.call_arg(&state.tree, id, i), annotation)
}
return
}
}
if (class == .expression and expected != no_node) {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .unary) {
view :: ast.unary_expr(&state.tree, id)
if (view.operator == .negate or view.operator == .bit_not or view.operator == .logical_not) {
try expand_expected(state, view.operand, expected)
return
}
}
if (kind == .binary and !is_type(state, expected)) {
view :: ast.binary_expr(&state.tree, id)
match view.operator {
.add, .subtract, .multiply, .divide, .bit_and, .bit_or, .bit_xor,
.logical_and, .logical_or: {
try expand_expected(state, view.left, expected)
try expand_expected(state, view.right, expected)
return
}
.shift_left, .shift_right, .shift_left_saturating: {
try expand_expected(state, view.left, expected)
try expand(state, view.right)
return
}
else: {}
}
}
if (kind == .try_expr) {
try expand_expected(state, ast.type_operand(&state.tree, id), expected)
return
}
if (kind == .match_expr) {
try expand(state, ast.match_expr(&state.tree, id).subject)
for (0..ast.match_arm_count(&state.tree, id)) |i| {
arm_id :: ast.match_arm(&state.tree, id, i)
arm :: ast.match_arm_view(&state.tree, arm_id)
for (0..ast.match_pattern_count(&state.tree, arm_id)) |j|
try expand(state, ast.match_pattern(&state.tree, arm_id, j))
state.yield_type = expected
annotation :: if (arm.expression) expected else no_node
try expand_expected(state, arm.body, annotation)
}
return
}
}
if (class == .expression) {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .record_literal or kind == .array_literal) {
annotation := expected
if (kind == .record_literal) {
if (ast.record_literal(&state.tree, id).type) |type| {
try expand(state, type)
annotation = type
}
}
shape := if (annotation != no_node) try canonical_type(state, annotation) else no_node
if (shape != no_node and ast.expr_kind(&state.tree, shape) == .fallible_type)
shape = try canonical_type(state, ast.binary_expr(&state.tree, shape).left)
if (shape != no_node and ast.expr_kind(&state.tree, shape) == .optional_type)
shape = try canonical_type(state, ast.type_operand(&state.tree, shape))
if (
kind == .array_literal and shape != no_node
and (ast.expr_kind(&state.tree, shape) == .array_type or ast.expr_kind(&state.tree, shape) == .slice_type)
) {
element :: if (ast.expr_kind(&state.tree, shape) == .array_type)
ast.array_type(&state.tree, shape).element else ast.slice_type(&state.tree, shape).element
for (0..ast.array_element_count(&state.tree, id)) |i|
try expand_expected(state, ast.array_element(&state.tree, id, i), element)
return
}
if (
kind == .record_literal and shape != no_node
and (ast.expr_kind(&state.tree, shape) == .record_type or ast.expr_kind(&state.tree, shape) == .union_type)
) {
for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
slot :: if (field.name != null) type_record_slot(state, shape, field.name?) else i
field_type := no_node
if (slot < ast.record_field_count(&state.tree, shape)) {
if (ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type) |type|
field_type = type
}
if (field.value) |value| try expand_expected(state, value, field_type)
}
return
}
}
}
count :: ast.child_count(&state.tree, id)
for (0..count) |i| {
child :: ast.child(&state.tree, id, i)
try expand(state, child)
}
if (!try type_array_projection(state, id)) {
try reject_evaluation(state, id)
return
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .binary) {
view :: ast.binary_expr(&state.tree, id)
if (view.operator == .bit_or
and (is_type(state, try expression_type(state, view.left, 0))
or is_type(state, try expression_type(state, view.right, 0)))) {
composed :: try ast.add_binary_type(state.builder, view.span, view.left, view.right, false)
try replace(state, id, composed)
try refresh(state)
return
}
if (
(view.operator == .equal or view.operator == .not_equal)
and branch_constant(state, view.left, 0) and branch_constant(state, view.right, 0)
) {
left :: try evaluate(state, view.left)
right :: try evaluate(state, view.right)
if (left.kind == .type and right.kind == .type) _ = try evaluate_binary(state, id)
}
}
if (usize(id) >= state.original_count and class == .expression and ast.expr_kind(&state.tree, id) == .array_type) {
view :: ast.array_type(&state.tree, id)
if (view.count) |count_node| {
try arraylist.append(&state.rewrites.constraints, count_node)
value :: try evaluate(state, count_node)
if (value.kind == .integer) {
folded :: try materialize(state, value)
replacement :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, id),
folded.node, view.element, view.mutable)
try replace(state, id, replacement)
try refresh(state)
}
}
}
}
@hide:file
value_block_terminal proc(state @State, id ast.NodeId) bool {
class :: ast.node_class(&state.tree, id)
if (class == .block) {
count :: ast.block_statement_count(&state.tree, id)
return count > 0 and value_block_terminal(state, ast.block_statement(&state.tree, id, count - 1))
}
if (class != .statement) return false
match ast.stmt_kind(&state.tree, id) {
.yield_stmt, .return_stmt, .break_stmt, .continue_stmt, .unreachable_stmt: return true
.block_stmt: return value_block_terminal(state, ast.statement_block(&state.tree, id))
.if_stmt: {
view :: ast.if_stmt(&state.tree, id)
return (
view.else_block != null
and value_block_terminal(state, view.then_block)
and value_block_terminal(state, view.else_block?)
)
}
else: return false
}
}
# reflection values are owned AST snapshots. only ordinary leaves and selected
# runtime bodies enter IR; metadata slices never retain interpreter addresses.
@hide:file
metadata_member proc(state @mut State, at ast.NodeId, text []u8) ast.NodeId ! Error {
result :: try ast.add_generated_identifier(state.builder, ast.node_span(&state.tree, at),
try strpool.intern(state.strings, text), text)
try refresh(state)
return result
}
@hide:file
metadata_package_contains proc(state @State, package ast.NodeId, source diagnostics.SourceId) bool {
for (0..ast.package_decl_count_at(&state.tree, package)) |i| {
if (ast.node_span(&state.tree, ast.package_decl_at(&state.tree, package, i)).source == source) return true
}
return false
}
@hide:file
metadata_tags proc(state @mut State, type ast.NodeId) ast.NodeId ! Error {
if (ast.expr_kind(&state.tree, type) == .enum_type) return type
if (ast.union_type(&state.tree, type).tag) |tag| return try canonical_type(state, tag)
members := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&members)
for (0..ast.record_field_count(&state.tree, type)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i))
member :: try ast.add_field(state.builder, field.span, field.name, null, null)
try arraylist.append(&members, member)
try refresh(state)
}
result :: try ast.add_enum_type(state.builder, ast.node_span(&state.tree, type), null, members.items)
try refresh(state)
return result
}
@hide:file
metadata_import proc(state @State, at ast.NodeId) ast.NodeId {
source :: ast.node_span(&state.tree, at).source
for (0..state.original_count) |i| {
imported :: ast.NodeId(i)
resolved :: binding(state, imported)
if (ast.node_class(&state.tree, imported) != .import or resolved.kind != .namespace) continue
if (!mem.eql(ast.source_text(&state.tree, ast.import_decl(&state.tree, imported).path), "@std/meta")) continue
other :: ast.node_span(&state.tree, imported).source
if (source == other) return resolved.data.namespace
if (metadata_package_contains(state, resolved.data.namespace, source)) return resolved.data.namespace
for (0..state.original_count) |j| {
package :: ast.NodeId(j)
if (package == state.tree.root or ast.node_class(&state.tree, package) != .package) continue
if (
metadata_package_contains(state, package, source)
and metadata_package_contains(state, package, other)
) return resolved.data.namespace
}
}
return no_node
}
@hide:file
metadata_type proc(state @mut State, package ast.NodeId, name []u8) ast.NodeId ! Error {
for (0..ast.package_decl_count_at(&state.tree, package)) |i| {
declaration :: ast.package_decl_at(&state.tree, package, i)
if (ast.node_class(&state.tree, declaration) != .statement) continue
if (mem.eql(strpool.get_str(state.strings, ast.name_id(&state.tree, declaration))?, name))
return try canonical_type(state, ast.statement_decl(&state.tree, declaration).initializer)
}
return no_node
}
@hide:file
metadata_record proc(state @mut State, at, type ast.NodeId, values []ast.NodeId) ast.NodeId ! Error {
try refresh(state)
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
span :: ast.node_span(&state.tree, at)
for (values) |value, i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i))
owned := value
if (!is_type(state, field.type?))
owned = try ast.add_typed_constant(state.builder, span, field.type?, value)
member :: try ast.add_field(state.builder, span, field.name, null, owned)
try arraylist.append(&fields, member)
try refresh(state)
}
literal :: try ast.add_record_literal(state.builder, span, null, fields.items)
result :: try ast.add_typed_constant(state.builder, span, type, literal)
try refresh(state)
return result
}
@hide:file
metadata_schema proc(state @mut State, type ast.NodeId, names [][]u8, types []ast.NodeId) bool ! Error {
if (
type == no_node or ast.expr_kind(&state.tree, type) != .record_type
or ast.record_field_count(&state.tree, type) != names.len
) return false
for (names) |name, i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i))
if (
field.name == null or field.type == null
or !mem.eql(ast.literal_text(&state.tree, field.name?), name)
or !try same_type(state, field.type?, types[i])
) return false
}
return true
}
@hide:file
metadata_abi proc(state @mut State, at ast.NodeId, types []ast.NodeId) bool ! Error {
info :: types[0]
if (
info == no_node or ast.expr_kind(&state.tree, info) != .union_type
or !ast.union_type(&state.tree, info).auto_tag
) return false
span :: ast.node_span(&state.tree, at)
byte_string :: try ast.add_slice_type(state.builder, span, try builtin(state, at, .u8), false, null)
optional_name :: try ast.add_optional_type(state.builder, span, byte_string)
fields :: try ast.add_slice_type(state.builder, span, types[2], false, null)
usize_type :: try builtin(state, at, .usize)
type_type :: try builtin(state, at, .type)
bool_type :: try builtin(state, at, .bool)
try refresh(state)
array_names [2][]u8 :: ["child", "len"]
array_types [2]ast.NodeId :: [type_type, usize_type]
field_names [3][]u8 :: ["name", "type", "index"]
field_types [3]ast.NodeId :: [byte_string, type_type, usize_type]
record_names [4][]u8 :: ["name", "fields", "is_tuple", "layout"]
record_types [4]ast.NodeId :: [optional_name, fields, bool_type, types[5]]
enum_names [1][]u8 :: ["fields"]
enum_types [1]ast.NodeId :: [fields]
if (
!try metadata_schema(state, types[1], array_names[..], array_types[..])
or !try metadata_schema(state, types[2], field_names[..], field_types[..])
or !try metadata_schema(state, types[3], record_names[..], record_types[..])
or !try metadata_schema(state, types[4], enum_names[..], enum_types[..])
) return false
layout :: types[5]
if (
layout == no_node or ast.expr_kind(&state.tree, layout) != .enum_type
or ast.enum_member_count(&state.tree, layout) != 2
) return false
for (0..usize(2)) |i| {
name :: ast.field(&state.tree, ast.enum_member(&state.tree, layout, i)).name
expected :: if (i == 0) "auto" else "c"
if (name == null or !mem.eql(ast.literal_text(&state.tree, name?), expected)) return false
}
names [18][]u8 :: ["invalid", "void", "noreturn", "anyopaque", "bool", "integer", "float",
"array", "pointer", "slice", "range", "optional", "function", "enum", "record", "union", "fallible", "distinct"]
if (ast.record_field_count(&state.tree, info) != names.len) return false
void_type :: try builtin(state, at, .void)
for (names) |name, i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, info, i))
expected := void_type
if (i == 7) expected = types[1]
if (i == 13) expected = types[4]
if (i == 14) expected = types[3]
if (i == 17) expected = type_type
if (
field.name == null or field.type == null
or !mem.eql(ast.literal_text(&state.tree, field.name?), name)
or !try same_type(state, field.type?, expected)
) return false
}
return true
}
@hide:file
expand_typeinfo proc(state @mut State, id ast.NodeId) void ! Error {
if (ast.call_arg_count(&state.tree, id) != 1) {
try reject(state, id, .argument_count_mismatch)
return
}
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
argument :: ast.call_arg(&state.tree, id, 0)
value :: try evaluate(state, argument)
if (value.kind != .type) {
if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id)
else try reject(state, id, .expected_type)
return
}
package :: metadata_import(state, id)
if (package == no_node) {
try reject(state, id, .missing_metadata_import)
return
}
types [6]mut ast.NodeId := undefined
names [6][]u8 :: ["TypeInfo", "ArrayInfo", "FieldInfo", "RecordInfo", "EnumInfo", "Layout"]
for (names) |name, i| {
types[i] = try metadata_type(state, package, name)
if (types[i] == no_node) {
try reject(state, id, .invalid_metadata_abi)
return
}
}
if (!try metadata_abi(state, id, types[..])) {
try reject(state, id, .invalid_metadata_abi)
return
}
type :: value.node
if (type_record_storage(state, type, 0)) {
if (!try type_record_check(state, type)) {
try reject_evaluation(state, id)
return
}
} else if (binding(state, type).kind != .builtin) {
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, argument), argument, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
}
kind :: ast.expr_kind(&state.tree, type)
tag []u8 := "invalid"
payload := no_node
shape := type
resolved :: binding(state, type)
if (resolved.kind == .builtin) {
builtin_kind :: builtin_identity(resolved.data.builtin)
if (try integer_type(state, type)) tag = "integer"
else if (builtin_kind == .float) tag = "float"
else if (builtin_kind == .bool) tag = "bool"
else if (builtin_kind == .void) tag = "void"
else if (builtin_kind == .anyopaque) tag = "anyopaque"
else if (builtin_kind == .string) tag = "pointer"
} else if (kind == .array_type) {
tag = "array"
array :: ast.array_type(&state.tree, type)
length := Value{}
if (array.count) |count| length = try evaluate(state, count)
if (length.kind != .integer or length.negative) {
try reject(state, id, .invalid_array_length)
return
}
child :: (try materialize(state, try evaluate(state, array.element))).node
count :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id), length.magnitude)
values [2]ast.NodeId :: [child, count]
payload = try metadata_record(state, id, types[1], values[..])
} else if (kind == .distinct_type) {
tag = "distinct"
payload = (try materialize(state, try evaluate(state, ast.type_operand(&state.tree, type)))).node
} else if (kind == .pointer_type) tag = "pointer"
else if (kind == .slice_type) tag = "slice"
else if (kind == .optional_type) tag = "optional"
else if (kind == .function_type) tag = "function"
else if (kind == .fallible_type) tag = "fallible"
else if (kind == .union_type) tag = "union"
else if (kind == .record_type) tag = "record"
else if (kind == .enum_type or kind == .union_tag_type) {
tag = "enum"
if (kind == .union_tag_type) {
variants :: try inline_match_shape(state, try canonical_type(state, ast.type_operand(&state.tree, type)))
shape = try metadata_tags(state, variants)
}
} else if (kind == .sum_type) {
shape = try inline_match_shape(state, type)
if (shape == no_node) {
try reject_evaluation(state, id)
return
}
tag = if (ast.expr_kind(&state.tree, shape) == .enum_type) "enum" else "union"
}
span :: ast.node_span(&state.tree, id)
if (mem.eql(tag, "record") or mem.eql(tag, "enum")) {
enumeration :: mem.eql(tag, "enum")
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
count :: if (enumeration) ast.enum_member_count(&state.tree, shape) else ast.record_field_count(&state.tree, shape)
tuple := !enumeration
for (0..count) |i| {
if (!try type_step(state, id)) {
try reject_evaluation(state, id)
return
}
field_id :: if (enumeration) ast.enum_member(&state.tree, shape, i)
else ast.record_field(&state.tree, shape, i)
field :: ast.field(&state.tree, field_id)
ordinal :: try ast.add_integer(state.builder, span, u64(i))
try refresh(state)
name := ordinal
if (field.name) |member| {
name = member
tuple = false
}
text :: try ast.add_string(state.builder, span, ast.literal_text(&state.tree, name))
child_type :: if (enumeration) type else field.type?
child :: (try materialize(state, try evaluate(state, child_type))).node
values [3]ast.NodeId :: [text, child, ordinal]
try arraylist.append(&fields, try metadata_record(state, id, types[2], values[..]))
}
collection :: try ast.add_array_literal(state.builder, span, fields.items)
if (enumeration) {
values [1]ast.NodeId :: [collection]
payload = try metadata_record(state, id, types[4], values[..])
} else {
name := try ast.add_literal(state.builder, .null, span)
try refresh(state)
for (state.declarations.items) |declaration| {
if (ast.node_class(&state.tree, declaration) == .statement
and ast.statement_decl(&state.tree, declaration).initializer == type) {
name = try ast.add_string(state.builder, span, strpool.get_str(state.strings, ast.name_id(&state.tree, declaration))?)
break
}
}
tuple_value :: try ast.add_boolean(state.builder, span, tuple)
try refresh(state)
layout_text :: if (ast.record_c_layout(&state.tree, type)) "c" else "auto"
layout_name :: try metadata_member(state, id, layout_text)
layout :: try ast.add_enum_literal(state.builder, span, layout_name)
values [4]ast.NodeId :: [name, collection, tuple_value, layout]
payload = try metadata_record(state, id, types[3], values[..])
}
}
if (payload == no_node) payload = try ast.add_literal(state.builder, .void, span)
member :: try metadata_member(state, id, tag)
field :: try ast.add_field(state.builder, span, member, null, payload)
fields [1]ast.NodeId :: [field]
literal :: try ast.add_record_literal(state.builder, span, null, fields[..])
result :: try ast.add_typed_constant(state.builder, span, types[0], literal)
try replace(state, id, result)
try refresh(state)
}
# erase only the name: ordinary selection checks the base and retains its place,
# evaluation order, mutability and active-union checks through both backends.
@hide:file
expand_field proc(state @mut State, id ast.NodeId) void ! Error {
if (ast.call_arg_count(&state.tree, id) != 2) {
try reject(state, id, .argument_count_mismatch)
return
}
base :: ast.call_arg(&state.tree, id, 0)
argument :: ast.call_arg(&state.tree, id, 1)
state.evaluation_diagnostic = diagnostics.no_diagnostic
value :: try evaluate(state, argument)
if (value.kind != .string) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.comptime_argument_required, ast.node_span(&state.tree, argument))
try reject_evaluation(state, id)
return
}
try arraylist.append(&state.rewrites.constraints, argument)
text :: ast.literal_text(&state.tree, value.node)
bytes := arraylist.init(u8, state.allocator)
defer arraylist.deinit(&bytes)
cursor := usize(1)
while (cursor < text.len - 1) {
byte :: ast.string_next(text, &cursor) catch {
try reject(state, id, .invalid_escape)
return
}
try arraylist.append(&bytes, byte)
}
# only canonical decimal names denote tuple ordinals; "01" is a name.
numeric := bytes.items.len > 0 and (bytes.items.len == 1 or bytes.items[0] != '0')
ordinal := u64(0)
for (bytes.items) |byte| {
if (byte < '0' or byte > '9') {
numeric = false
break
}
digit :: u64(byte - '0')
if (ordinal > divtrunc!(maxval!(u64) - digit, u64(10))) {
numeric = false
break
}
ordinal = ordinal * 10 + digit
}
span :: ast.node_span(&state.tree, argument)
member := no_node
if (numeric) member = try ast.add_integer(state.builder, span, ordinal)
else {
interned :: try strpool.intern(state.strings, bytes.items)
member = try ast.add_generated_identifier(state.builder, span, interned, bytes.items)
}
selection :: try ast.add_selection(state.builder, ast.node_span(&state.tree, id), base, member)
try replace(state, id, selection)
try refresh(state)
state.expanded.items[usize(id)] = 0
try expand(state, id)
}
# literal collections deliberately retain per-field context: generated records
# may pair heterogeneous types and optional defaults in arrays as well as tuples.
@hide:file
struct_collection proc(state @mut State, input ast.NodeId) ast.NodeId ! Error {
if (!try type_step(state, input)) return no_node
try expand(state, input)
resolved :: binding(state, input)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
return no_node
}
if (resolved.kind == .declaration
and ast.node_class(&state.tree, resolved.data.declaration) == .statement) {
declaration :: ast.statement_decl(&state.tree, resolved.data.declaration)
if (declaration.mutability != .constant) return no_node
if (declaration.type) |annotation| {
value :: try type_execution_value(state, declaration.initializer, annotation)
if (value == no_node) return no_node
return try struct_collection(state, value)
}
return try struct_collection(state, declaration.initializer)
}
kind :: ast.expr_kind(&state.tree, input)
if (kind == .array_literal) return input
if (kind == .record_literal) {
if (ast.record_literal(&state.tree, input).type != null) {
annotation :: try expression_type(state, input, 0)
value :: try type_execution_value(state, input, annotation)
if (value == no_node) return no_node
return try struct_collection(state, value)
}
for (0..ast.record_field_count(&state.tree, input)) |i| {
if (ast.field(&state.tree, ast.record_field(&state.tree, input, i)).name != null) return no_node
}
return input
}
if (kind == .typed_constant) {
value :: ast.typed_constant(&state.tree, input)
annotation :: try canonical_type(state, value.type)
if (annotation == no_node) return no_node
if (ast.expr_kind(&state.tree, annotation) != .array_type) {
if (ast.expr_kind(&state.tree, annotation) != .record_type) return no_node
for (0..ast.record_field_count(&state.tree, annotation)) |i| {
if (ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).name != null) return no_node
}
}
if (!try type_array_initialized(state, input, input)) return no_node
return input
}
value :: try evaluate(state, input)
if (value.kind != .aggregate or value.annotation == no_node) return no_node
owned :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, input),
value.annotation, value.node)
try refresh(state)
return try struct_collection(state, owned)
}
@hide:file
struct_collection_count proc(state @State, input ast.NodeId) usize {
value :: if (ast.expr_kind(&state.tree, input) == .typed_constant)
ast.typed_constant(&state.tree, input).value else input
return if (ast.expr_kind(&state.tree, value) == .array_literal)
ast.array_element_count(&state.tree, value) else ast.record_field_count(&state.tree, value)
}
@hide:file
struct_collection_item proc(state @mut State, input ast.NodeId, index usize) ast.NodeId ! Error {
value := input
annotation := no_node
if (ast.expr_kind(&state.tree, input) == .typed_constant) {
view :: ast.typed_constant(&state.tree, input)
value = view.value
type :: try canonical_type(state, view.type)
if (ast.expr_kind(&state.tree, type) == .array_type) annotation = ast.array_type(&state.tree, type).element
else annotation = ast.field(&state.tree, ast.record_field(&state.tree, type, index)).type?
}
element :: if (ast.expr_kind(&state.tree, value) == .array_literal)
ast.array_element(&state.tree, value, index)
else ast.field(&state.tree, ast.record_field(&state.tree, value, index)).value?
if (annotation == no_node or is_type(state, annotation)
or ast.expr_kind(&state.tree, element) == .typed_constant) return element
result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, element), annotation, element)
try refresh(state)
return result
}
@hide:file
expand_struct_type proc(state @mut State, id ast.NodeId) void ! Error {
if (ast.call_arg_count(&state.tree, id) != 4) {
try reject(state, id, .argument_count_mismatch)
return
}
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
layout_input :: ast.call_arg(&state.tree, id, 0)
try expand(state, layout_input)
layout := layout_input
if (ast.expr_kind(&state.tree, layout) != .enum_literal) {
value :: try evaluate_ir(state, layout, try expression_type(state, layout, 0))
layout = value.node
}
if (layout == no_node or ast.expr_kind(&state.tree, layout) != .enum_literal) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_struct_layout, ast.node_span(&state.tree, layout_input))
try reject_evaluation(state, id)
return
}
layout_name :: ast.literal_text(&state.tree, ast.enum_literal(&state.tree, layout))
c_layout :: mem.eql(layout_name, "c")
if (!c_layout and !mem.eql(layout_name, "auto")) {
try reject(state, id, .invalid_struct_layout)
return
}
collections [3]mut ast.NodeId := undefined
for (0..usize(3)) |i| {
collections[i] = try struct_collection(state, ast.call_arg(&state.tree, id, i + 1))
if (collections[i] == no_node) {
try reject_evaluation(state, id)
return
}
}
count :: struct_collection_count(state, collections[0])
if (count != struct_collection_count(state, collections[1])
or count != struct_collection_count(state, collections[2])) {
try reject(state, id, .array_length_mismatch)
return
}
if (c_layout and count == 0) {
try reject(state, id, .invalid_struct_layout)
return
}
if (count > state.type_execution_remaining) {
try reject(state, id, .comptime_execution_limit)
return
}
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
bytes := arraylist.init(u8, state.allocator)
defer arraylist.deinit(&bytes)
for (0..count) |i| {
if (!try type_step(state, id)) {
try reject_evaluation(state, id)
return
}
name_input :: try struct_collection_item(state, collections[0], i)
name_value :: try evaluate(state, name_input)
if (name_value.kind != .string) {
try reject_evaluation(state, id)
return
}
bytes.items = bytes.items[..0]
text :: ast.literal_text(&state.tree, name_value.node)
cursor := usize(1)
while (cursor < text.len - 1) {
byte :: ast.string_next(text, &cursor) catch {
try reject(state, id, .invalid_escape)
return
}
valid :: (
byte == '_' or (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z')
or (bytes.items.len > 0 and byte >= '0' and byte <= '9')
)
if (!valid) {
try reject(state, id, .invalid_generated_field_name)
return
}
try arraylist.append(&bytes, byte)
}
if (bytes.items.len == 0) {
try reject(state, id, .invalid_generated_field_name)
return
}
span :: ast.node_span(&state.tree, name_input)
name :: try ast.add_generated_identifier(state.builder, span,
try strpool.intern(state.strings, bytes.items), bytes.items)
try refresh(state)
for (fields.items) |previous| {
if (ast.name_id(&state.tree, ast.field(&state.tree, previous).name?) == ast.name_id(&state.tree, name)) {
try reject(state, id, .duplicate_field)
return
}
}
type_input :: try struct_collection_item(state, collections[1], i)
type_value :: try evaluate(state, type_input)
if (type_value.kind != .type) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_record_field_type, ast.node_span(&state.tree, type_input))
try reject_evaluation(state, id)
return
}
field_type :: (try materialize(state, type_value)).node
default_input :: try struct_collection_item(state, collections[2], i)
optional :: try ast.add_optional_type(state.builder, span, field_type)
contextual :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, default_input), optional, default_input)
capture :: try ast.add_identifier(state.builder, span, strpool.no_str)
try refresh(state)
present :: try type_capture_guard(state, contextual, capture)
if (present.kind != .boolean) {
try reject_evaluation(state, id)
return
}
default ?ast.NodeId := null
if (present.boolean) {
unwrapped :: try ast.add_unary(state.builder,
ast.node_span(&state.tree, default_input), .unwrap, contextual)
try refresh(state)
value :: try type_execution_value(state, unwrapped, field_type)
if (value == no_node) {
try reject_evaluation(state, id)
return
}
default = value
}
field :: try ast.add_field(state.builder, span, name, field_type, default)
try refresh(state)
try arraylist.append(&fields, field)
}
record :: try ast.add_record_type(state.builder, ast.node_span(&state.tree, id), fields.items)
if (c_layout) ast.set_record_c_layout(state.builder, record)
try replace(state, id, record)
try refresh(state)
state.expanded.items[usize(id)] = 0
try expand(state, id)
}
# composed struct variants use declaration IDs internally, but generated syntax
# must not embed those declarations and make dependency discovery treat them as owned.
@hide:file
member_name proc(state @mut State, name ast.NodeId) ast.NodeId ! Error {
if (ast.node_class(&state.tree, name) != .statement) return name
result :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, name), ast.name_id(&state.tree, name))
try refresh(state)
return result
}
# flatten source member names and payload annotations only; ordinary checking still
# validates composition, variant identity and collisions on the original sum type.
@hide:file
inline_sum_fields proc(state @mut State, input ast.NodeId, fields @mut arraylist.ArrayList(ast.NodeId)) bool ! Error {
if (!try type_step(state, input)) return false
type :: try canonical_type(state, input)
if (type == no_node) return false
kind :: ast.expr_kind(&state.tree, type)
if (kind == .sum_type) {
sum :: ast.binary_expr(&state.tree, type)
if (!try inline_sum_fields(state, sum.left, fields)) return false
return try inline_sum_fields(state, sum.right, fields)
}
if (kind == .record_type) {
for (0..ast.node_count(&state.tree)) |i| {
declaration :: ast.NodeId(i)
if (ast.node_class(&state.tree, declaration) != .statement
or ast.stmt_kind(&state.tree, declaration) != .declaration) continue
if (ast.statement_decl(&state.tree, declaration).initializer != type) continue
name :: try member_name(state, declaration)
field :: try ast.add_field(state.builder, ast.node_span(&state.tree, type), name, type, null)
try refresh(state)
try arraylist.append(fields, field)
return true
}
return false
}
if (kind != .enum_type and kind != .union_type) return false
count :: if (kind == .enum_type) ast.enum_member_count(&state.tree, type)
else ast.record_field_count(&state.tree, type)
for (0..count) |i| {
if (!try type_step(state, input)) return false
member :: if (kind == .enum_type) ast.enum_member(&state.tree, type, i)
else ast.record_field(&state.tree, type, i)
field :: ast.field(&state.tree, member)
if (field.name == null) return false
if (kind == .union_type and field.type == null) return false
duplicate := false
for (fields.items) |previous| {
if (ast.name_id(&state.tree, ast.field(&state.tree, previous).name?)
== ast.name_id(&state.tree, field.name?)) duplicate = true
}
if (duplicate) continue
payload :: if (kind == .enum_type) try builtin(state, member, .void) else field.type?
generated :: try ast.add_field(state.builder, field.span, field.name, payload, null)
try refresh(state)
try arraylist.append(fields, generated)
}
return true
}
@hide:file
inline_match_shape proc(state @mut State, annotation ast.NodeId) ast.NodeId ! Error {
if (ast.expr_kind(&state.tree, annotation) != .sum_type) return annotation
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
if (!try inline_sum_fields(state, annotation, &fields)) return no_node
tagged := false
for (fields.items) |member| {
payload :: try canonical_type(state, ast.field(&state.tree, member).type?)
if (payload == no_node) return no_node
resolved :: binding(state, payload)
if (resolved.kind != .builtin or resolved.data.builtin != .void) tagged = true
}
span :: ast.node_span(&state.tree, annotation)
result :: if (tagged) try ast.add_union_type(state.builder, span, null, true, fields.items)
else try ast.add_enum_type(state.builder, span, null, fields.items)
try refresh(state)
return result
}
# expand dispatch, not the subject: ordinary checking/lowering retains its single
# evaluation and by-value payload captures. only tags are compile-time constants.
@hide:file
expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! Error {
inline_arm := no_node
for (0..ast.match_arm_count(&state.tree, id)) |i| {
candidate :: ast.match_arm(&state.tree, id, i)
if (ast.match_arm_view(&state.tree, candidate).is_inline) {
inline_arm = candidate
break
}
}
if (inline_arm == no_node) return
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
arm :: ast.match_arm_view(&state.tree, inline_arm)
for (0..usize(2)) |i| {
capture :: if (i == 0) arm.capture else arm.tag_capture
if (capture) |name| {
resolved :: binding(state, name)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
try reject_evaluation(state, id)
return
}
}
}
count :: ast.match_arm_count(&state.tree, id)
if (ast.match_arm(&state.tree, id, count - 1) != inline_arm) {
try reject(state, id, .unreachable_match_arm)
return
}
subject :: ast.match_expr(&state.tree, id).subject
try expand(state, subject)
annotation := subject_type
if (annotation == no_node) annotation = try expression_type(state, subject, 0)
if (annotation == no_node) {
try reject(state, id, .invalid_inline_match_subject)
return
}
kind :: ast.expr_kind(&state.tree, annotation)
tag_only :: kind == .union_tag_type
members_type := annotation
if (tag_only) members_type = try canonical_type(state, ast.type_operand(&state.tree, annotation))
if (members_type != no_node) members_type = try inline_match_shape(state, members_type)
if (members_type == no_node) {
if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id)
else try reject(state, id, .invalid_sum_type)
return
}
tagged := false
if (!tag_only and ast.expr_kind(&state.tree, members_type) == .union_type) {
view :: ast.union_type(&state.tree, members_type)
tagged = view.auto_tag or view.tag != null
}
if (ast.expr_kind(&state.tree, members_type) != .enum_type and !tag_only and !tagged) {
try reject(state, id, .invalid_inline_match_subject)
return
}
if (!tagged and arm.tag_capture_present) {
try reject(state, id, .invalid_inline_match_capture)
return
}
arms := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arms)
for (0..(count - 1)) |i| {
prior :: ast.match_arm(&state.tree, id, i)
if (ast.match_arm_view(&state.tree, prior).catch_all) {
try reject(state, id, .unreachable_match_arm)
return
}
try arraylist.append(&arms, prior)
}
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = ast.NodeId(0), result_type = no_node,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
span :: ast.node_span(&state.tree, inline_arm)
static_capture :: if (tagged) arm.tag_capture else arm.capture
tag_type := annotation
if (static_capture != null) {
if (tagged) {
union_view :: ast.union_type(&state.tree, members_type)
if (kind != .sum_type and union_view.tag != null)
tag_type = try canonical_type(state, union_view.tag?)
else if (type_record_storage(state, annotation, 0)) tag_type = try metadata_tags(state, annotation)
else {
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
tag_type = try ast.add_union_tag_type(state.builder, span, concrete.node)
try refresh(state)
}
}
concrete_tag :: try materialize(state, Value{ kind = .type, node = tag_type })
tag_type = concrete_tag.node
}
union_members :: tagged or tag_only
members :: if (union_members) ast.record_field_count(&state.tree, members_type)
else ast.enum_member_count(&state.tree, members_type)
for (0..members) |i| {
member :: if (union_members) ast.record_field(&state.tree, members_type, i)
else ast.enum_member(&state.tree, members_type, i)
field :: ast.field(&state.tree, member)
if (field.name == null or (tagged and field.type == null)) {
try reject(state, id, .invalid_record_field_type)
return
}
covered := false
for (arms.items) |prior| {
for (0..ast.match_pattern_count(&state.tree, prior)) |j| {
pattern :: ast.match_pattern(&state.tree, prior, j)
if (ast.expr_kind(&state.tree, pattern) == .enum_literal
and ast.name_id(&state.tree, ast.enum_literal(&state.tree, pattern))
== ast.name_id(&state.tree, field.name?)) covered = true
}
}
if (covered) continue
if (!try type_step(state, inline_arm)) {
try reject_evaluation(state, id)
return
}
execution.locals.items = execution.locals.items[..0]
mem.set(execution.mapping, no_node)
literal :: try ast.add_enum_literal(state.builder, span, field.name?)
if (static_capture) |capture| {
# runtime patterns use the union's dispatch enum, not its declared tag enum.
tag_literal :: try ast.add_enum_literal(state.builder, span, field.name?)
tag :: try ast.add_typed_constant(state.builder, span, tag_type, tag_literal)
try refresh(state)
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = tag, annotation = tag_type,
})
}
payload_capture ?ast.NodeId := null
if (tagged and arm.capture != null) {
payload_type :: if (is_type(state, field.type?)) field.type? else try canonical_type(state, field.type?)
if (payload_type == no_node) {
try reject(state, id, .invalid_record_field_type)
return
}
if (binding(state, payload_type).kind == .builtin
and binding(state, payload_type).data.builtin == .void) {
value :: try ast.add_literal(state.builder, .void, span)
try refresh(state)
try arraylist.append(&execution.locals, TypeLocal{
declaration = arm.capture?, value = value, annotation = payload_type,
})
} else {
name :: arm.capture?
payload_capture = try ast.add_identifier(state.builder,
ast.node_span(&state.tree, name), ast.name_id(&state.tree, name))
try refresh(state)
state.bindings.items[usize(payload_capture?)] = sema.Binding{
kind = .declaration, data = sema.BindingData{ declaration = payload_capture? },
}
execution.mapping[usize(name)] = payload_capture?
hashmap.put(&state.capture_types, payload_capture?, payload_type) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
}
}
body :: try type_snapshot(state, &execution, arm.body)
patterns [1]ast.NodeId :: [literal]
generated :: try ast.add_match_arm(state.builder, span, patterns[..],
payload_capture, body, arm.expression, false)
try refresh(state)
try arraylist.append(&arms, generated)
}
if (arms.items.len == count - 1) {
try reject(state, id, .unreachable_match_arm)
return
}
replacement :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), subject, arms.items)
try replace(state, id, replacement)
try refresh(state)
}
# runtime bodies share the type-producer selector, but retain ordinary runtime
# parameters and control transfers in the selected arm.
@hide:file
specialize_match proc(state @mut State, id, statement, expected ast.NodeId) bool ! Error {
explicit :: ast.expr_kind(&state.tree, ast.match_expr(&state.tree, id).subject) == .comptime_expr
try expand_inline_match(state, id, no_node)
if (ast.expr_kind(&state.tree, id) != .match_expr) return true
subject :: ast.match_expr(&state.tree, id).subject
annotation := no_node
for (0..ast.match_arm_count(&state.tree, id)) |i| {
arm_id :: ast.match_arm(&state.tree, id, i)
arm :: ast.match_arm_view(&state.tree, arm_id)
if (explicit or arm.capture == null or ast.match_pattern_count(&state.tree, arm_id) == 0) continue
if (hashmap.get(&state.capture_types, arm.capture?) != null) continue
if (annotation == no_node) {
annotation = try expression_type(state, subject, 0)
if (annotation != no_node) annotation = try inline_match_shape(state, annotation)
}
if (annotation == no_node) break
payload := no_node
if (ast.match_expr(&state.tree, id).optional_capture) {
if (ast.expr_kind(&state.tree, annotation) != .optional_type) break
payload = ast.type_operand(&state.tree, annotation)
} else {
if (ast.expr_kind(&state.tree, annotation) != .union_type) break
pattern :: ast.match_pattern(&state.tree, arm_id, 0)
if (ast.expr_kind(&state.tree, pattern) != .enum_literal) continue
slot :: type_record_slot(state, annotation, ast.enum_literal(&state.tree, pattern))
if (slot == no_index) continue
field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot))
if (field.type) |type| payload = type
}
if (payload != no_node) {
hashmap.put(&state.capture_types, arm.capture?, payload) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
}
}
try expand(state, subject)
metadata :: type_record_storage(state, try expression_type(state, subject, 0), 0)
if (!explicit and usize(id) < state.original_count and !metadata) return false
if (!explicit and !metadata and !branch_constant(state, subject, 0)) return false
state.evaluation_diagnostic = diagnostics.no_diagnostic
index :: try type_match_arm(state, id)
if (index == no_index) {
try reject_evaluation(state, id)
return true
}
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, index))
selected := try type_match_body(state, id, arm)
if (selected != no_node and !arm.expression) {
selected = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, selected), selected)
try refresh(state)
}
if (selected == no_node) {
try reject_evaluation(state, id)
return true
}
try expand_expected(state, selected, expected)
if (statement != no_node) {
replacement :: try ast.add_expression_stmt(state.builder, selected)
try replace(state, statement, replacement)
} else {
resolved :: binding(state, selected)
try remember(state, id)
ast.replace_node_from_source(state.builder, id, selected)
try refresh(state)
state.bindings.items[usize(id)] = resolved
}
try refresh(state)
return true
}
# only probe side-effect-free expressions. ordinary calls stay runtime unless the
# source requests evaluation with $(); probing must not execute FFI or emit traps.
@hide:file
branch_constant proc(state @State, id ast.NodeId, depth usize) bool {
if (depth >= 128) return false
resolved :: binding(state, id)
if (resolved.kind == .builtin) return true
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
if (ast.node_class(&state.tree, target) == .function) return true
if (ast.node_class(&state.tree, target) != .statement) return false
declaration :: ast.statement_decl(&state.tree, target)
initializer :: ast.expr_kind(&state.tree, declaration.initializer)
# contextual aggregates may omit defaults with runtime calls.
if (declaration.type != null and (initializer == .record_literal or initializer == .array_literal)) return false
return (
declaration.mutability == .constant
and branch_constant(state, declaration.initializer, depth + 1)
)
}
match ast.expr_kind(&state.tree, id) {
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void,
.union_constant: return true
.typed_constant: return branch_constant(state, ast.typed_constant(&state.tree, id).value, depth + 1)
.enum_literal: return true
.record_literal: {
# typed literals may omit defaults; materialized constants are complete
# contextual literals inside a typed_constant wrapper.
for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
if (field.value == null or !branch_constant(state, field.value?, depth + 1)) return false
}
return ast.record_literal(&state.tree, id).type == null
}
.array_literal: {
for (0..ast.array_element_count(&state.tree, id)) |i| {
if (!branch_constant(state, ast.array_element(&state.tree, id, i), depth + 1)) return false
}
return true
}
.selection: return branch_constant(state, ast.selection_expr(&state.tree, id).base, depth + 1)
.index: {
view :: ast.index_expr(&state.tree, id)
return branch_constant(state, view.base, depth + 1) and branch_constant(state, view.index, depth + 1)
}
.unary: {
view :: ast.unary_expr(&state.tree, id)
return (
(view.operator == .negate or view.operator == .logical_not or view.operator == .bit_not)
and branch_constant(state, view.operand, depth + 1)
)
}
.binary: {
view :: ast.binary_expr(&state.tree, id)
return (
branch_constant(state, view.left, depth + 1)
and branch_constant(state, view.right, depth + 1)
)
}
.pointer_type: return branch_constant(state, ast.pointer_type(&state.tree, id).element, depth + 1)
.optional_type, .alias_expr: return branch_constant(state, ast.type_operand(&state.tree, id), depth + 1)
.record_type, .union_type, .enum_type, .distinct_type, .array_type, .slice_type, .function_type,
.fallible_type, .sum_type, .union_tag_type: return true
else: return false
}
}
@hide:file
builtin proc(state @mut State, at ast.NodeId, kind sema.BuiltinType) ast.NodeId ! Error {
id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str)
try refresh(state)
state.bindings.items[usize(id)] = sema.Binding{ kind = .builtin, data = sema.BindingData{ builtin = kind } }
return id
}
@hide:file
reference proc(state @mut State, at, target ast.NodeId) ast.NodeId ! Error {
id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str)
try refresh(state)
state.bindings.items[usize(id)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = target } }
return id
}
@hide:file
is_type proc(state @State, id ast.NodeId) bool {
value :: binding(state, id)
return value.kind == .builtin and value.data.builtin == .type
}
# recognize erased storage without evaluating dependent lengths or elaborating
# bodies. pointers retain the runtime materialization boundary.
@hide:file
type_storage_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize) bool {
if (depth >= 128 or usize(input) >= bindings.len) return false
resolved :: bindings[usize(input)]
if (resolved.kind == .builtin) return resolved.data.builtin == .type
if (resolved.kind == .declaration) {
declaration :: resolved.data.declaration
if (ast.node_class(tree, declaration) != .statement) return false
view :: ast.statement_decl(tree, declaration)
return (view.mutability == .constant
and type_storage_annotation(tree, bindings, view.initializer, depth + 1))
}
kind :: ast.expr_kind(tree, input)
if (kind == .alias_expr)
return type_storage_annotation(tree, bindings, ast.type_operand(tree, input), depth + 1)
if (kind == .array_type)
return type_storage_annotation(tree, bindings, ast.array_type(tree, input).element, depth + 1)
if (kind == .slice_type)
return type_storage_annotation(tree, bindings, ast.slice_type(tree, input).element, depth + 1)
if (kind != .record_type and kind != .union_type) return false
for (0..ast.record_field_count(tree, input)) |i| {
field :: ast.field(tree, ast.record_field(tree, input, i))
if (field.type != null and type_storage_annotation(tree, bindings, field.type?, depth + 1)) return true
}
return false
}
@hide:file
compiletime_result proc(state @State, annotation ast.NodeId) bool {
return type_record_storage(state, annotation, 0)
}
@hide:file
inference_contextual proc(state @State, id ast.NodeId) bool {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .literal_null or kind == .literal_string) return true
if (ast.expr_kind(&state.tree, id) == .record_literal) {
annotation :: ast.record_literal(&state.tree, id).type
return annotation == null or ast.expr_kind(&state.tree, annotation?) == .enum_literal
}
return flexible(state, id) or contextual_literal(state, id)
}
@hide:file
inference_phase proc(state @State, id ast.NodeId) usize {
if (inference_contextual(state, id)) return 2
if (ast.expr_kind(&state.tree, id) == .call) {
callee :: binding(state, ast.call_expr(&state.tree, id).callee)
if (
callee.kind == .declaration and ast.node_class(&state.tree, callee.data.declaration) == .function
and template(state, callee.data.declaration)
and ast.call_arg_count(&state.tree, id) != ast.function_param_count(&state.tree, callee.data.declaration)
) return 1
}
return 0
}
@hide:file
annotation_ready proc(
state @State, function, id ast.NodeId, arguments []Value, depth usize,
) bool {
if (depth >= 128) return false
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
index :: parameter_index(state, function, resolved.data.declaration)
if (index != no_index) return arguments[index].kind != .invalid
}
for (0..ast.child_count(&state.tree, id)) |i| {
if (!annotation_ready(state, function, ast.child(&state.tree, id, i), arguments, depth + 1)) return false
}
return true
}
@hide:file
specialize_call proc(state @mut State, call, function, expected ast.NodeId) void ! Error {
if (state.depth >= 128 or state.instances.items.len >= 4096) {
try reject(state, call, .specialization_limit)
return
}
state.depth += 1
defer state.depth -= 1
count :: ast.function_param_count(&state.tree, function)
# type-containing results have no runtime call: every input is specialized.
type_result :: compiletime_result(state, ast.function_decl(&state.tree, function).return_type)
runtime := usize(0)
for (0..count) |i| {
if (!type_result and !ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1
}
supplied :: ast.call_arg_count(&state.tree, call)
explicit :: supplied == count
if (!explicit and (type_result or supplied != runtime)) {
try reject(state, call, .argument_count_mismatch)
return
}
arguments :: try mem.alloc(Value, state.allocator, count)
defer mem.free(state.allocator, arguments)
mem.set(arguments, Value{})
runtime_arguments := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&runtime_arguments)
position := 0
for (0..count) |i| {
parameter :: ast.function_param(&state.tree, function, i)
view :: ast.param(&state.tree, parameter)
if (view.comptime and !explicit) continue
argument :: ast.call_arg(&state.tree, call, position)
position += 1
if (type_result or view.comptime) {
context_annotation :: try argument_annotation(state, function, view.type, arguments)
try expand_expected(state, argument, context_annotation)
state.evaluation_diagnostic = diagnostics.no_diagnostic
annotation := view.type
contextual := contextual_literal(state, argument)
if (!contextual and !is_type(state, annotation)) {
type :: try canonical_type(state, annotation)
contextual = type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type
}
if (contextual or type_record_storage(state, annotation, 0))
annotation = try argument_annotation(state, function, view.type, arguments)
arguments[i] = try evaluate_expected(state, argument, annotation)
if (arguments[i].kind == .invalid) {
try reject_evaluation(state, call)
return
}
if (is_type(state, view.type) != (arguments[i].kind == .type)) {
try reject(state, call, .type_mismatch)
return
}
} else {
try arraylist.append(&runtime_arguments, argument)
}
}
if (!explicit) {
# rigid arguments constrain first; result context then types nested generic
# calls before flexible literals choose their default widths.
for (0..usize(3)) |phase| {
if (phase == 1 and expected != no_node) {
result :: ast.function_decl(&state.tree, function).return_type
if (!try infer_result(state, function, result, expected, arguments)) {
try reject(state, call, .cannot_infer_generic)
return
}
}
position = 0
for (0..count) |i| {
parameter :: ast.function_param(&state.tree, function, i)
view :: ast.param(&state.tree, parameter)
if (view.comptime) continue
argument :: runtime_arguments.items[position]
position += 1
contextual :: inference_contextual(state, argument)
if (inference_phase(state, argument) != phase) continue
if (annotation_ready(state, function, view.type, arguments, 0)) {
annotation :: try argument_annotation(state, function, view.type, arguments)
try expand_expected(state, argument, annotation)
}
actual :: try expression_type(state, argument, 0)
# contextual literals need not have an independent type; the rewritten
# call checks them against the substituted signature after inference.
if (
(actual == no_node or !try infer(state, function, view.type, actual, arguments, contextual))
and !contextual
) {
try reject(state, call, .cannot_infer_generic)
return
}
}
}
}
for (0..count) |i| {
parameter :: ast.function_param(&state.tree, function, i)
if (ast.param(&state.tree, parameter).comptime and arguments[i].kind == .invalid) {
try reject(state, call, .cannot_infer_generic)
return
}
}
position = 0
for (0..count) |i| {
view :: ast.param(&state.tree, ast.function_param(&state.tree, function, i))
if (view.comptime or type_result) continue
argument :: runtime_arguments.items[position]
position += 1
if (state.expanded.items[usize(argument)] != 0) continue
annotation :: try argument_annotation(state, function, view.type, arguments)
try expand_expected(state, argument, annotation)
}
key := usize(function)
for arguments |argument| {
if (argument.kind != .invalid) key = mix(key, try value_hash(state, argument))
}
head := no_index
if (hashmap.get(&state.cache, key)) |found| head = found
current := head
instance := no_index
while (current != no_index) {
entry :: state.instances.items[current]
equal := entry.function == function and entry.arguments.len == count
if (equal) {
for (0..count) |i| {
if (!try value_equal(state, entry.arguments[i], arguments[i])) {
equal = false
break
}
}
}
if (equal) {
instance = current
break
}
current = entry.next
}
if (instance == no_index) {
for (0..count) |i| {
if (arguments[i].kind != .invalid) arguments[i] = try materialize(state, arguments[i])
}
values :: try mem.alloc(Value, state.allocator, count)
memcopy!(values, arguments)
instance = state.instances.items.len
arraylist.append(&state.instances, Instance{ function = function, arguments = values, next = head }) catch |err| {
mem.free(state.allocator, values)
return err
}
# cache heads are replaced without reallocating a second entry for the same hash.
if (head != no_index) {
for (state.cache.entries) |@entry| {
if (entry.hash != 0 and entry.key == key) {
entry.value = instance
break
}
}
} else {
hashmap.put(&state.cache, key, instance) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
}
try instantiate(state, instance, call)
if (binding(state, call).kind == .invalid) {
state.instances.items[instance].diagnostic = binding(state, call).data.diagnostic
return
}
}
diagnostic :: state.instances.items[instance].diagnostic
if (diagnostic != diagnostics.no_diagnostic) {
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 } }
return
}
target :: state.instances.items[instance].declaration
if (target == no_node and state.instances.items[instance].value == no_node) {
try reject(state, call, .specialization_cycle)
return
}
if (explicit) {
signature :: state.instances.items[instance].signature
for (0..count) |i| {
parameter :: ast.function_param(&state.tree, signature, i)
view :: ast.param(&state.tree, parameter)
if (
(!type_result and !view.comptime) or is_type(state, view.type)
or type_record_storage(state, view.type, 0)
) continue
original :: ast.call_arg(&state.tree, call, i)
constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
}
}
replacement := state.instances.items[instance].value
if (replacement == no_node) {
callee :: try reference(state, call, target)
if (state.instances.items[instance].type != no_node)
replacement = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, call), callee, true)
else replacement = try ast.add_call(state.builder, ast.node_span(&state.tree, call), callee, runtime_arguments.items)
}
try replace(state, call, replacement)
try refresh(state)
}
@hide:file
instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
function :: state.instances.items[index].function
type_result :: compiletime_result(state, ast.function_decl(&state.tree, function).return_type)
count :: state.original_count
clone_start :: ast.node_count(&state.tree)
mapping :: state.mapping
mem.set(mapping, no_node)
parameters :: ast.function_param_count(&state.tree, function)
for (0..parameters) |i| {
parameter :: ast.function_param(&state.tree, function, i)
if (!type_result and !ast.param(&state.tree, parameter).comptime) continue
value :: state.instances.items[index].arguments[i]
replacement := value.node
annotation :: ast.param(&state.tree, parameter).type
if (!is_type(state, annotation)) {
concrete_type :: try ast.clone_subtree(state.builder, annotation, mapping)
replacement = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, parameter), concrete_type, value.node)
try refresh(state)
}
for (0..count) |n| {
resolved :: binding(state, ast.NodeId(n))
if (
resolved.kind == .declaration and resolved.data.declaration == parameter
and ast.node_class(&state.tree, ast.NodeId(n)) == .expression
) mapping[n] = replacement
}
}
clone :: try ast.clone_subtree(state.builder, function, mapping)
try refresh(state)
# copy original resolution, remapping references to cloned locals and runtime parameters.
for (0..count) |i| {
target :: mapping[i]
if (target == no_node or usize(target) < clone_start) continue
if (ast.node_class(&state.tree, target) == .expression and ast.expr_kind(&state.tree, target) == .typed_constant) continue
value := state.bindings.items[i]
if (value.kind == .declaration) {
original :: value.data.declaration
if (original != function and usize(original) < count and mapping[usize(original)] != no_node)
value.data.declaration = mapping[usize(original)]
}
state.bindings.items[usize(target)] = value
}
state.instances.items[index].signature = try ast.copy_node(state.builder, clone)
try refresh(state)
view :: ast.function_decl(&state.tree, clone)
for (0..parameters) |i| {
parameter :: ast.function_param(&state.tree, clone, i)
annotation :: ast.param(&state.tree, parameter).type
if ((type_result or ast.param(&state.tree, parameter).comptime) and !is_type(state, annotation)) {
if (!try valid_value(state, annotation, state.instances.items[index].arguments[i])) {
value :: state.instances.items[index].arguments[i]
code :: if (value.kind == .integer and try integer_type(state, annotation))
diagnostics.Code.literal_out_of_range else diagnostics.Code.type_mismatch
try reject(state, call, code)
return
}
}
}
if (type_result) {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
constraints_start :: state.rewrites.constraints.items.len
returned :: try return_type_value(state, view.body, ast.NodeId(clone_start), view.return_type)
if (returned == no_node) {
try reject_type_result(state, call, returned, constraints_start)
return
}
if (!is_type(state, view.return_type)) {
state.instances.items[index].value = returned
constraint_count :: state.rewrites.constraints.items.len
for (constraints_start..constraint_count) |i| {
constraint :: state.rewrites.constraints.items[i]
if (!try expand_type_matches(state, constraint)) {
try reject_type_result(state, call, returned, constraints_start)
return
}
try expand(state, constraint)
}
return
}
result := returned
kind :: ast.expr_kind(&state.tree, returned)
if (kind != .record_type and kind != .union_type and kind != .enum_type and kind != .distinct_type) {
result = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, returned), returned, true)
try refresh(state)
}
declaration :: try ast.add_statement_decl(state.builder, view.span, view.name,
ast.name_id(&state.tree, function), null, .constant, result)
try refresh(state)
state.instances.items[index].declaration = declaration
state.instances.items[index].type = returned
state.bindings.items[usize(declaration)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = declaration } }
try arraylist.append(&state.declarations, declaration)
if (!try expand_type_matches(state, declaration)) {
try reject_type_result(state, call, result, constraints_start)
return
}
try expand(state, declaration)
constraint_count :: state.rewrites.constraints.items.len
for (constraints_start..constraint_count) |i| {
constraint :: state.rewrites.constraints.items[i]
if (!try expand_type_matches(state, constraint)) {
try reject_type_result(state, call, result, constraints_start)
return
}
try expand(state, constraint)
}
} else {
runtime := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&runtime)
for (0..parameters) |i| {
parameter :: ast.function_param(&state.tree, clone, i)
if (!ast.param(&state.tree, parameter).comptime) try arraylist.append(&runtime, parameter)
}
concrete :: try ast.add_function_decl(state.builder, view.span, view.name,
ast.name_id(&state.tree, function), view.return_type, runtime.items, view.body)
ast.replace_node(state.builder, clone, concrete)
try refresh(state)
state.instances.items[index].declaration = clone
state.bindings.items[usize(clone)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = clone } }
try arraylist.append(&state.declarations, clone)
try expand(state, clone)
}
}
@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 } }
}
# failed deferred elaboration must not leave unchecked type expressions in the package.
@hide:file
reject_type_result proc(state @mut State, call, result ast.NodeId, constraints_start usize) void ! Error {
try reject_evaluation(state, call)
state.evaluation_diagnostic = binding(state, call).data.diagnostic
if (result != no_node) try reject_evaluation(state, result)
for (constraints_start..(state.rewrites.constraints.items.len)) |i|
try reject_evaluation(state, state.rewrites.constraints.items[i])
}
@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
if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .typed_constant) {
if (!try type_array_initialized(state, id, id)) return false
}
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
if ((flags[usize(target)] & 8) == 0 and (flags[usize(target)] & 2) == 0) {
class :: ast.node_class(&state.tree, target)
if (class == .parameter) return false
if (class == .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
evaluation_owned proc(state @State, id ast.NodeId, flags []mut u8) void {
if ((flags[usize(id)] & 8) != 0) return
flags[usize(id)] = flags[usize(id)] | 8
for (0..ast.child_count(&state.tree, id)) |i| evaluation_owned(state, ast.child(&state.tree, id, i), flags)
}
@hide:file
contextual_literal proc(state @State, id ast.NodeId) bool {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .builtin_call)
return mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name), "some")
if (kind == .unary) return ast.unary_expr(&state.tree, id).operator == .bit_not and flexible(state, id)
if (kind == .binary) {
operator :: ast.binary_expr(&state.tree, id).operator
return (
(operator == .bit_and or operator == .bit_or or operator == .bit_xor
or operator == .shift_left or operator == .shift_right or operator == .shift_left_saturating)
and flexible(state, id)
)
}
return kind == .record_literal or kind == .array_literal or kind == .enum_literal or kind == .block_expr
}
@hide:file
annotation_generic proc(state @State, function, id ast.NodeId, depth usize) bool {
if (depth >= 128) return true
resolved :: binding(state, id)
if (
resolved.kind == .declaration
and parameter_index(state, function, resolved.data.declaration) != no_index
) return true
for (0..ast.child_count(&state.tree, id)) |i| {
if (annotation_generic(state, function, ast.child(&state.tree, id, i), depth + 1)) return true
}
return false
}
@hide:file
argument_annotation proc(
state @mut State, function, annotation ast.NodeId, arguments []Value,
) ast.NodeId ! Error {
if (!annotation_generic(state, function, annotation, 0)) return annotation
mapping :: try mem.alloc(ast.NodeId, state.allocator, state.original_count)
defer mem.free(state.allocator, mapping)
mem.set(mapping, no_node)
for (0..ast.function_param_count(&state.tree, function)) |i| {
if (arguments[i].kind == .invalid) continue
parameter :: ast.function_param(&state.tree, function, i)
value := try materialize(state, arguments[i])
if (value.kind != .type and value.annotation != no_node) {
value.node = try ast.add_typed_constant(state.builder,
ast.node_span(&state.tree, value.node), value.annotation, value.node)
try refresh(state)
}
for (0..mapping.len) |n| {
resolved :: binding(state, ast.NodeId(n))
if (
resolved.kind == .declaration and resolved.data.declaration == parameter
and ast.node_class(&state.tree, ast.NodeId(n)) == .expression
) mapping[n] = value.node
}
}
start :: ast.node_count(&state.tree)
result :: try ast.clone_subtree(state.builder, annotation, mapping)
try refresh(state)
for (0..mapping.len) |i| {
target :: mapping[i]
if (target != no_node and usize(target) >= start) state.bindings.items[usize(target)] = state.bindings.items[i]
}
return result
}
@hide:file
evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error {
try expand_expected(state, id, annotation)
if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
constant :: try type_execution_value(state, id, annotation)
if (constant == no_node or !try type_array_initialized(state, constant, id)) return Value{}
value :: ast.typed_constant(&state.tree, constant)
return Value{ kind = .aggregate, node = value.value, annotation = value.type }
}
if (!is_type(state, annotation)) {
type :: try canonical_type(state, annotation)
if (type != no_node and ast.expr_kind(&state.tree, type) == .array_type
and !try type_array_initialized(state, id, id)) return Value{}
if (type != no_node and (contextual_literal(state, id)
or ast.expr_kind(&state.tree, id) == .block_expr
or ast.expr_kind(&state.tree, id) == .slice
or ast.expr_kind(&state.tree, type) == .fallible_type))
return try evaluate_ir(state, id, type)
}
return try evaluate(state, id)
}
# raw union snapshots cannot distinguish addresses from overlapping integer bits.
# require every view to be address-free before retaining the complete representation.
@hide:file
union_bytes_materializable proc(checked @typecheck.Result, type typecheck.Type, depth usize) bool {
if (depth >= 128) return false
match type.kind {
.int, .i8, .i16, .i32, .isize, .u8, .u16, .u32, .u64, .usize,
.float, .bool, .void, .enumeration: return true
.array, .optional: {
return union_bytes_materializable(checked, typecheck.array_type(checked, type.array_type).element, depth + 1)
}
.fallible: {
channels :: typecheck.array_type(checked, type.array_type)
return (union_bytes_materializable(checked, channels.element, depth + 1)
and union_bytes_materializable(checked, channels.error_type, depth + 1))
}
.record: {
for (typecheck.record_type(checked, type.record_type).fields) |field| {
if (!union_bytes_materializable(checked, field.type, depth + 1)) return false
}
return true
}
else: return false
}
}
# values leaving temporary evaluation must own no interpreter addresses or module IDs.
# snapshot readonly bytes and rebuild source literals/references before releasing their backing.
@hide:file
serialize_value proc(
state @mut State,
module @ir.Module,
checked @typecheck.Result,
outcome @interpreter.Outcome,
functions []ast.NodeId,
type typecheck.Type,
value ir.Value,
span ast.Span,
) ast.NodeId ! Error {
uninitialized := value.uninitialized
if (ir.aggregate(value.type)) {
if (module.arrays.items[usize(value.array_type)].is_union)
uninitialized = ir.union_tag(module, value).uninitialized
else if (type.kind == .optional)
uninitialized = ir.aggregate_item(module, value, 0).uninitialized
}
if (uninitialized) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
return no_node
}
result := no_node
if (type.kind == .slice) {
slice :: typecheck.array_type(checked, type.array_type)
if (slice.mutable or slice.element.kind != .u8 or slice.element.nominal != no_node) return no_node
pointer :: ir.aggregate_item(module, value, 0)
length :: ir.aggregate_item(module, value, 1)
if (pointer.uninitialized or length.uninitialized) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
return no_node
}
if (
length.data.unsigned != 0
and interpreter.evaluation_byte(outcome, module, pointer, usize(length.data.unsigned - 1)) == null
) return no_node
nonzero_sentinel :: slice.sentinel != null and slice.sentinel? != 0
length_bytes :: usize(length.data.unsigned)
if (nonzero_sentinel and length_bytes == maxval!(usize)) return no_node
extra :: if (nonzero_sentinel) usize(1) else usize(0)
bytes :: try mem.alloc(u8, state.allocator, length_bytes + extra)
defer mem.free(state.allocator, bytes)
for (0..length_bytes) |index| {
byte :: interpreter.evaluation_byte(outcome, module, pointer, index)
if (byte == null) return no_node
if (byte?.uninitialized) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
return no_node
}
bytes[index] = u8(byte?.data.unsigned)
}
if (slice.sentinel != null) {
sentinel :: interpreter.evaluation_byte(outcome, module, pointer, length_bytes)
if (sentinel == null) return no_node
if (sentinel?.uninitialized) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
return no_node
}
if (sentinel?.data.unsigned != u64(slice.sentinel?)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .sentinel_mismatch, span)
return no_node
}
if (nonzero_sentinel) bytes[length_bytes] = u8(slice.sentinel?)
}
result = try ast.add_string(state.builder, span, bytes)
if (nonzero_sentinel) {
end :: try ast.add_integer(state.builder, span, u64(length_bytes))
sentinel :: try ast.add_integer(state.builder, span, u64(slice.sentinel?))
result = try ast.add_slice(state.builder, span, result, null, end, sentinel)
}
} else if (type.kind == .array) {
array :: typecheck.array_type(checked, type.array_type)
elements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&elements)
for (0..array.len) |i| {
child :: ir.aggregate_item(module, value, i)
node :: try serialize_value(state, module, checked, outcome, functions, array.element, child, span)
if (node == no_node) return no_node
try arraylist.append(&elements, node)
}
result = try ast.add_array_literal(state.builder, span, elements.items)
} else if (type.kind == .record) {
record :: typecheck.record_type(checked, type.record_type)
if (record.is_union and record.tag_type.kind != .enumeration) {
if (!union_bytes_materializable(checked, type, 0)) return no_node
bytes :: try mem.alloc(u8, state.allocator, value.data.elements.len)
defer mem.free(state.allocator, bytes)
for (value.data.elements) |byte, index| {
if (byte.uninitialized) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
return no_node
}
bytes[index] = u8(byte.data.unsigned)
}
annotation :: if (type.nominal != no_node) type.nominal
else if (record.declaration != no_node) record.declaration else record.source
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
literal :: try ast.add_string(state.builder, span, bytes)
result = try ast.add_union_constant(state.builder, span, concrete.node, literal)
try refresh(state)
return result
}
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
for (record.fields) |field, i| {
if (record.is_union and !ir.union_active(module, value, i)) continue
if (record.is_union and field.type.kind == .void) {
name :: try member_name(state, field.name?)
result = try ast.add_enum_literal(state.builder, span, name)
try refresh(state)
return result
}
child :: ir.aggregate_item(module, value, i)
node :: try serialize_value(state, module, checked, outcome, functions, field.type, child, span)
if (node == no_node) return no_node
name := field.name
if (name != null) name = try member_name(state, name?)
generated :: try ast.add_field(state.builder, span, name, null, node)
try refresh(state)
try arraylist.append(&fields, generated)
}
result = try ast.add_record_literal(state.builder, span, null, fields.items)
} else if (type.kind == .fallible) {
container :: typecheck.array_type(checked, type.array_type)
failed :: ir.union_tag(module, value).data.integer == 1
index :: if (failed) usize(1) else usize(0)
channel :: if (failed) container.error_type else container.element
child :: ir.aggregate_item(module, value, index)
result = try serialize_value(state, module, checked, outcome, functions, channel, child, span)
if (result == no_node) return no_node
# keep named success/error types distinct even when their members overlap.
annotation := no_node
if (channel.nominal != no_node) annotation = channel.nominal
else if (channel.kind == .enumeration) annotation = typecheck.enum_type(checked, channel.enum_type).declaration
else if (channel.kind == .record) annotation = typecheck.record_type(checked, channel.record_type).declaration
if (annotation != no_node) {
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
result = try ast.add_typed_constant(state.builder, span, concrete.node, result)
}
} else if (type.kind == .enumeration) {
enumeration :: typecheck.enum_type(checked, type.enum_type)
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned
for (enumeration.members) |member| {
if (member.magnitude == magnitude and member.negative == negative) {
name :: try member_name(state, member.name)
result = try ast.add_enum_literal(state.builder, span, name)
break
}
}
} else if (type.kind == .optional) {
container :: typecheck.array_type(checked, type.array_type)
if (container.element.kind == .pointer or container.element.kind == .function) {
absent :: if (container.element.kind == .function)
value.data.function == maxval!(ir.FunctionId)
else value.data.unsigned == maxval!(u64)
if (absent) result = try ast.add_literal(state.builder, .null, span)
else result = try serialize_value(state, module, checked, outcome, functions, container.element, value, span)
} else if (!ir.aggregate_item(module, value, 0).data.boolean) {
result = try ast.add_literal(state.builder, .null, span)
} else {
child :: ir.aggregate_item(module, value, 1)
payload :: try serialize_value(state, module, checked, outcome, functions, container.element, child, span)
if (payload == no_node) return no_node
name :: try ast.add_generated_identifier(state.builder, span,
try strpool.intern(state.strings, "some"), "some")
arguments [1]ast.NodeId :: [payload]
result = try ast.add_builtin_call(state.builder, span, name, arguments[..])
}
} else if (type.kind == .function) {
index :: usize(value.data.function)
if (index >= functions.len or functions[index] == no_node) return no_node
result = try reference(state, functions[index], functions[index])
} else if (ir.integer_bits(value.type) != 0 and type.kind != .pointer) {
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned
result = try ast.add_integer(state.builder, span, magnitude)
if (negative) result = try ast.add_unary(state.builder, span, .negate, result)
} else if (type.kind == .float) {
result = try ast.add_float(state.builder, span, value.data.real)
} else if (type.kind == .bool) {
result = try ast.add_boolean(state.builder, span, value.data.boolean)
} else if (type.kind == .string) {
result = try ast.add_string(state.builder, span, ir.string_bytes(module, value.data.string))
} else if (type.kind == .void) {
result = try ast.add_literal(state.builder, .void, span)
}
try refresh(state)
return result
}
@hide:file
constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
kind :: ast.expr_kind(&state.tree, left)
if (kind != ast.expr_kind(&state.tree, right)) return false
if (kind == .literal_void) return true
if (kind == .typed_constant or kind == .union_constant) {
a :: if (kind == .union_constant) ast.union_constant(&state.tree, left) else ast.typed_constant(&state.tree, left)
b :: if (kind == .union_constant) ast.union_constant(&state.tree, right) else ast.typed_constant(&state.tree, right)
return try same_type_context(state, a.type, b.type, true) and try constant_equal(state, a.value, b.value)
}
if (kind == .slice) {
a :: ast.slice_expr(&state.tree, left)
b :: ast.slice_expr(&state.tree, right)
# serialized nonzero-sentinel views have a canonical owned string and explicit end.
return (
try constant_equal(state, a.base, b.base)
and try constant_equal(state, a.end?, b.end?)
and try constant_equal(state, a.sentinel?, b.sentinel?)
)
}
if (kind == .array_literal) {
count :: ast.array_element_count(&state.tree, left)
if (count != ast.array_element_count(&state.tree, right)) return false
for (0..count) |i| {
if (!try constant_equal(state, ast.array_element(&state.tree, left, i), ast.array_element(&state.tree, right, i))) return false
}
return true
}
if (kind == .record_literal) {
count :: ast.record_field_count(&state.tree, left)
if (count != ast.record_field_count(&state.tree, right)) return false
for (0..count) |i| {
a :: ast.field(&state.tree, ast.record_field(&state.tree, left, i))
b :: ast.field(&state.tree, ast.record_field(&state.tree, right, i))
if ((a.name == null) != (b.name == null)) return false
if (a.name != null and ast.name_id(&state.tree, a.name?) != ast.name_id(&state.tree, b.name?)) return false
if (!try constant_equal(state, a.value?, b.value?)) return false
}
return true
}
if (kind == .builtin_call and contextual_literal(state, left) and contextual_literal(state, right)) {
return try constant_equal(state, ast.call_arg(&state.tree, left, 0), ast.call_arg(&state.tree, right, 0))
}
if (kind == .enum_literal)
return ast.name_id(&state.tree, ast.enum_literal(&state.tree, left)) == ast.name_id(&state.tree, ast.enum_literal(&state.tree, right))
return try value_equal(state, try evaluate(state, left), try evaluate(state, right))
}
@hide:file
constant_hash proc(state @mut State, id ast.NodeId) usize ! Error {
kind :: ast.expr_kind(&state.tree, id)
hash :: enum_index(kind)
if (kind == .literal_void) return hash
if (kind == .typed_constant or kind == .union_constant) {
value :: if (kind == .union_constant) ast.union_constant(&state.tree, id) else ast.typed_constant(&state.tree, id)
return mix(mix(hash, try type_hash_context(state, value.type, true)), try constant_hash(state, value.value))
}
if (kind == .slice) {
view :: ast.slice_expr(&state.tree, id)
return mix(mix(mix(hash, try constant_hash(state, view.base)),
try constant_hash(state, view.end?)), try constant_hash(state, view.sentinel?))
}
if (kind == .array_literal) {
result := hash
for (0..ast.array_element_count(&state.tree, id)) |i|
result = mix(result, try constant_hash(state, ast.array_element(&state.tree, id, i)))
return result
}
if (kind == .record_literal) {
result := hash
for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
if (field.name) |name| result = mix(result, usize(ast.name_id(&state.tree, name)))
result = mix(result, try constant_hash(state, field.value?))
}
return result
}
if (kind == .builtin_call and contextual_literal(state, id))
return mix(hash, try constant_hash(state, ast.call_arg(&state.tree, id, 0)))
if (kind == .enum_literal) return mix(hash, usize(ast.name_id(&state.tree, ast.enum_literal(&state.tree, id))))
return try value_hash(state, try evaluate(state, id))
}
@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
function := no_node
if (callee.kind == .function) {
resolved :: binding(state, callee.node)
if (resolved.kind != .declaration) return Value{}
function = resolved.data.declaration
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| {
argument :: ast.call_arg(&state.tree, call, i)
value := Value{}
if (function != no_node and i < ast.function_param_count(&state.tree, function)) {
annotation :: ast.param(&state.tree, ast.function_param(&state.tree, function, i)).type
value = try evaluate_expected(state, argument, annotation)
} else value = try evaluate(state, argument)
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)
try refresh(state)
return try evaluate_ir(state, invocation, result_type)
}
@hide:file
evaluate_ir proc(state @mut State, expression, result_type ast.NodeId) Value ! Error {
if (result_type == no_node) return Value{}
span :: ast.node_span(&state.tree, expression)
returned :: try ast.add_return_stmt(state.builder, span, expression)
statements [1]ast.NodeId :: [returned]
body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
return try evaluate_ir_body(state, expression, body, result_type)
}
@hide:file
evaluate_ir_body proc(state @mut State, at, body, result_type ast.NodeId) Value ! Error {
span :: ast.node_span(&state.tree, at)
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)
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))
flags.items[usize(declaration)] = flags.items[usize(declaration)] | 2
evaluation_owned(state, declaration, flags.items)
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| {
if (diagnostic.custom_message) |text| {
_ = try diagnostics.add_message(&diagnostic_store, diagnostic.code, diagnostic.span, text)
} else _ = try diagnostics.add(&diagnostic_store, diagnostic.code, diagnostic.span)
}
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) {
diagnostic :: diagnostic_store.list.items[original_diagnostics]
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span)
return Value{}
}
functions := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&functions)
module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked,
wrapper, &functions, &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
source_type :: typecheck.type_of(&checked, result_type)
node :: try serialize_value(state, &module, &checked, &outcome, functions.items, source_type, value, span)
if (node == no_node) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
return Value{}
}
if (
(ir.aggregate(value.type) and source_type.kind != .slice)
or (source_type.kind == .slice and ast.expr_kind(&state.tree, node) == .slice)
or source_type.kind == .enumeration or source_type.kind == .void
)
return Value{ kind = .aggregate, node = node, annotation = result_type }
result := try evaluate(state, node)
result.annotation = result_type
return result
}
@hide:file
TypeLocal :: struct {
declaration ast.NodeId
value ast.NodeId
annotation ast.NodeId
}
@hide:file
TypeExecution :: struct {
locals arraylist.ArrayList(TypeLocal)
mapping []mut ast.NodeId
first_owned ast.NodeId
returned ast.NodeId = no_node
result_type ast.NodeId
yielded ast.NodeId = no_node
yield_type ast.NodeId = no_node
yield_depth usize = 0
cleanup_yield_depth usize = 0
cleanup_depth usize = 0
cleanup_loop_depth usize = 0
evaluate_values bool = false
pending_flow TypeFlow = .next
loop_depth usize = 0
inline_body ast.NodeId = no_node
inline_blocks ?@mut arraylist.ArrayList(ast.NodeId) = null
}
@hide:file
TypeFlow :: enum { next, returned, yielded, break, continue, invalid }
# type values cannot enter runtime IR. execute their control flow here, substituting
# owned constants into snapshots; ordinary value operations still use checked IR.
@hide:file
return_type_value proc(state @mut State, block, first_owned, result_type ast.NodeId) ast.NodeId ! Error {
# ponytail: snapshots recheck scalar IR; cap type-control work at 4096 steps
# until type values can share one persistent checked execution frame.
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = first_owned,
result_type = result_type,
evaluate_values = true,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
if (try execute_type_block(state, &execution, block) != .returned) return no_node
return execution.returned
}
@hide:file
evaluate_type_value_block proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = ast.NodeId(0),
result_type = no_node,
evaluate_values = true,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
return try execute_type_value_block(state, &execution, ast.block_expr(&state.tree, expression), annotation)
}
@hide:file
execute_type_value_block proc(
state @mut State, execution @mut TypeExecution, block, annotation ast.NodeId,
) ast.NodeId ! Error {
saved_value :: execution.yielded
saved_type :: execution.yield_type
execution.yielded = no_node
execution.yield_type = annotation
execution.yield_depth += 1
defer {
execution.yielded = saved_value
execution.yield_type = saved_type
execution.yield_depth -= 1
}
flow :: try execute_type_block(state, execution, block)
if (flow == .yielded) return execution.yielded
if (flow != .next) execution.pending_flow = flow
else {
inferred :: if (annotation == no_node) try value_block_type(state, block, 0) else annotation
result :: binding(state, inferred)
if (result.kind == .builtin and result.data.builtin == .void) {
value :: try ast.add_literal(state.builder, .void, ast.node_span(&state.tree, block))
try refresh(state)
return value
}
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.missing_yield, ast.node_span(&state.tree, block))
execution.pending_flow = .invalid
}
return no_node
}
@hide:file
type_step proc(state @mut State, at ast.NodeId) bool ! Error {
if (state.type_execution_remaining == 0) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.comptime_execution_limit, ast.node_span(&state.tree, at))
return false
}
state.type_execution_remaining -= 1
return true
}
@hide:file
prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) void ! Error {
if (id == no_node or usize(id) >= execution.mapping.len or execution.pending_flow != .next) return
# substituted arguments belong to the caller, including nominal type identity.
if (id < execution.first_owned) {
execution.mapping[usize(id)] = id
return
}
execution.mapping[usize(id)] = no_node
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
for (execution.locals.items) |local| {
if (local.declaration == resolved.data.declaration) {
execution.mapping[usize(id)] = local.value
return
}
}
}
if (execution.evaluate_values and ast.node_class(&state.tree, id) == .field) {
field :: ast.field(&state.tree, id)
if (field.type != null and field.value != null) {
annotation :: try type_snapshot(state, execution, field.type?)
if (annotation == no_node) return
value :: try type_snapshot_expected(state, execution, field.value?, annotation)
execution.mapping[usize(field.value?)] = value
if (value == no_node and execution.pending_flow == .next) execution.pending_flow = .invalid
return
}
}
if (execution.evaluate_values and ast.node_class(&state.tree, id) == .expression) {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .block_expr) {
execution.mapping[usize(id)] = try execute_type_value_block(state, execution,
ast.block_expr(&state.tree, id), no_node)
if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next)
execution.pending_flow = .invalid
return
}
if (kind == .match_expr) {
execution.mapping[usize(id)] = try type_snapshot_match(state, execution, id, no_node)
if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next)
execution.pending_flow = .invalid
return
}
if (kind == .binary) {
view :: ast.binary_expr(&state.tree, id)
if (view.operator == .logical_and or view.operator == .logical_or) {
boolean :: try builtin(state, id, .bool)
left :: try type_snapshot_expected(state, execution, view.left, boolean)
if (left == no_node) return
value :: try evaluate_ir(state, left, boolean)
if (value.kind != .boolean) {
execution.pending_flow = .invalid
return
}
right := no_node
if (value.boolean == (view.operator == .logical_or)) {
saved :: execution.evaluate_values
execution.evaluate_values = false
right = try type_snapshot(state, execution, view.right)
if (right != no_node and !try skip_type_value_blocks(state, right)) {
execution.pending_flow = .invalid
return
}
execution.evaluate_values = saved
} else right = try type_snapshot_expected(state, execution, view.right, boolean)
if (right == no_node and execution.pending_flow == .next) {
execution.pending_flow = .invalid
return
}
# retain the operator and skipped operand for ordinary type validation.
execution.mapping[usize(view.left)] = left
execution.mapping[usize(view.right)] = right
return
}
}
if (kind == .catch_expr) {
execution.mapping[usize(id)] = try type_snapshot_catch(state, execution, id, no_node)
if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next)
execution.pending_flow = .invalid
return
}
}
for (0..ast.child_count(&state.tree, id)) |i|
try prepare_type_snapshot(state, execution, ast.child(&state.tree, id, i))
}
@hide:file
bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start ast.NodeId) void ! Error {
if (usize(id) >= execution.mapping.len) return
target :: execution.mapping[usize(id)]
if (target == no_node or target < start) return
resolved := binding(state, id)
if (resolved.kind == .declaration) {
declaration :: resolved.data.declaration
if (usize(declaration) < execution.mapping.len and execution.mapping[usize(declaration)] != no_node)
resolved.data.declaration = execution.mapping[usize(declaration)]
}
state.bindings.items[usize(target)] = resolved
if (hashmap.get(&state.capture_types, id)) |annotation| {
hashmap.put(&state.capture_types, target, annotation) catch |err| {
match err {
.key_exists: {}
else: return err
}
}
}
for (0..ast.child_count(&state.tree, id)) |i|
try bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start)
}
@hide:file
skip_type_value_blocks proc(state @mut State, id ast.NodeId) bool ! Error {
if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .block_expr) {
annotation :: try expression_type(state, id, 0)
if (annotation == no_node) return false
span :: ast.node_span(&state.tree, id)
# skipped type-control bodies retain only their result type for checked IR.
placeholder :: try ast.add_literal(state.builder, .undefined, span)
local :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str,
annotation, .variable, placeholder)
try refresh(state)
yielded :: try ast.add_yield_stmt(state.builder, span, try reference(state, id, local))
statements [2]ast.NodeId :: [local, yielded]
body :: try ast.add_block(state.builder, span, statements[..])
replacement :: try ast.add_block_expr(state.builder, span, body)
ast.replace_node_from_source(state.builder, id, replacement)
try refresh(state)
return true
}
for (0..ast.child_count(&state.tree, id)) |i|
if (!try skip_type_value_blocks(state, ast.child(&state.tree, id, i))) return false
return true
}
@hide:file
type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
if (id == no_node or execution.pending_flow != .next) return no_node
if (execution.mapping.len < ast.node_count(&state.tree)) {
previous :: execution.mapping.len
execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree))
mem.set(execution.mapping[previous..], no_node)
}
try prepare_type_snapshot(state, execution, id)
if (execution.pending_flow != .next) return no_node
start :: ast.NodeId(ast.node_count(&state.tree))
result :: try ast.clone_subtree(state.builder, id, execution.mapping)
try refresh(state)
try bind_type_snapshot(state, execution, id, start)
return result
}
@hide:file
type_snapshot_expected proc(
state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId,
) ast.NodeId ! Error {
if (id == no_node or execution.pending_flow != .next) return no_node
if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .block_expr)
return try execute_type_value_block(state, execution, ast.block_expr(&state.tree, id), annotation)
if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .match_expr)
return try type_snapshot_match(state, execution, id, annotation)
if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .catch_expr)
return try type_snapshot_catch(state, execution, id, annotation)
return try type_snapshot(state, execution, id)
}
@hide:file
type_snapshot_catch proc(
state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId,
) ast.NodeId ! Error {
view :: ast.catch_expr(&state.tree, id)
operand :: try type_snapshot(state, execution, view.operand)
if (operand == no_node) return no_node
try expand_expected(state, operand, annotation)
operand_type :: try expression_type(state, operand, 0)
if (operand_type == no_node or ast.expr_kind(&state.tree, operand_type) != .fallible_type) return no_node
channels :: ast.binary_expr(&state.tree, operand_type)
owned :: try type_execution_value(state, operand, operand_type)
if (owned == no_node) return no_node
span :: ast.node_span(&state.tree, id)
boolean :: try builtin(state, id, .bool)
yes :: try ast.add_boolean(state.builder, span, true)
no :: try ast.add_boolean(state.builder, span, false)
failed :: try ast.add_return_stmt(state.builder, span, yes)
failure_statements [1]ast.NodeId :: [failed]
handler :: try ast.add_block(state.builder, span, failure_statements[..])
caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true)
discarded :: try ast.add_expression_stmt(state.builder, caught)
success :: try ast.add_return_stmt(state.builder, span, no)
statements [2]ast.NodeId :: [discarded, success]
body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
guard :: try evaluate_ir_body(state, id, body, boolean)
if (guard.kind != .boolean) return no_node
payload :: ast.typed_constant(&state.tree, owned).value
if (!guard.boolean) return try type_execution_value(state, payload, channels.left)
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
if (view.capture) |capture| {
error :: try type_execution_value(state, payload, channels.right)
if (error == no_node) return no_node
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = error, annotation = channels.right,
})
}
result_type := channels.left
if (annotation != no_node and !is_type(state, annotation)) {
contextual :: try canonical_type(state, annotation)
if (contextual != no_node) {
kind :: ast.expr_kind(&state.tree, contextual)
if (kind == .optional_type
and try same_type(state, ast.type_operand(&state.tree, contextual), channels.left))
result_type = annotation
if (kind == .fallible_type
and try same_type(state, ast.binary_expr(&state.tree, contextual).left, channels.left))
result_type = annotation
}
}
if (view.handler_is_block)
return try execute_type_value_block(state, execution, view.handler, result_type)
selected :: try type_snapshot_expected(state, execution, view.handler, result_type)
return try type_execution_value(state, selected, result_type)
}
@hide:file
type_dispatch_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, id).subject)
if (subject == no_node) return no_node
saved :: execution.evaluate_values
execution.evaluate_values = false
defer execution.evaluate_values = saved
snapshot :: try type_snapshot(state, execution, id)
if (snapshot == no_node) return no_node
arms := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arms)
for (0..ast.match_arm_count(&state.tree, snapshot)) |i|
try arraylist.append(&arms, ast.match_arm(&state.tree, snapshot, i))
optional_capture :: ast.match_expr(&state.tree, id).optional_capture
result :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), subject, arms.items)
if (optional_capture) ast.set_match_optional(state.builder, result)
try refresh(state)
return result
}
@hide:file
type_snapshot_match proc(
state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId,
) ast.NodeId ! Error {
snapshot :: try type_dispatch_snapshot(state, execution, id)
if (snapshot == no_node) return no_node
index :: try type_match_arm(state, snapshot)
if (index == no_index) return no_node
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, index))
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
if (arm.capture) |capture| {
value :: try type_match_capture(state, snapshot)
if (value == no_node) return no_node
constant :: ast.typed_constant(&state.tree, value)
payload :: if (is_type(state, constant.type)) constant.value else value
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture,
value = payload,
annotation = constant.type,
})
}
if (!arm.expression) return try execute_type_value_block(state, execution, arm.body, annotation)
return try type_snapshot_expected(state, execution, arm.body, annotation)
}
@hide:file
nominal_type_value proc(state @State, id ast.NodeId) bool {
kind :: ast.expr_kind(&state.tree, id)
return kind == .record_type or kind == .union_type or kind == .enum_type or kind == .distinct_type
}
# transfers targeting an inline loop must be selected before checking runtime
# bodies. nested loops own their transfers and are elaborated independently.
@hide:file
inline_has_transfer proc(state @State, id ast.NodeId) bool {
class :: ast.node_class(&state.tree, id)
if (class == .function) return false
if (class == .statement) {
kind :: ast.stmt_kind(&state.tree, id)
if (kind == .for_stmt or kind == .while_stmt) return false
if (kind == .break_stmt or kind == .continue_stmt) return true
}
for (0..ast.child_count(&state.tree, id)) |i| {
if (inline_has_transfer(state, ast.child(&state.tree, id, i))) return true
}
return false
}
@hide:file
inline_statement proc(state @mut State, id ast.NodeId) TypeFlow ! Error {
kind :: ast.stmt_kind(&state.tree, id)
if (kind == .break_stmt) return .break
if (kind == .continue_stmt) return .continue
if (kind == .unreachable_stmt) return .returned
if (kind == .yield_stmt or kind == .return_stmt) {
try expand(state, id)
return if (kind == .yield_stmt) TypeFlow.yielded else TypeFlow.returned
}
if (kind == .block_stmt)
return try inline_block(state, ast.statement_block(&state.tree, id))
selected := no_node
value_source := false
if (kind == .if_stmt) {
conditional :: ast.if_stmt(&state.tree, id)
explicit :: ast.expr_kind(&state.tree, conditional.condition) == .comptime_expr
condition :: if (explicit) ast.comptime_operand(&state.tree, conditional.condition) else conditional.condition
try expand(state, condition)
if (conditional.capture == null and (explicit or branch_constant(state, condition, 0))) {
value :: try evaluate_ir(state, condition, try builtin(state, condition, .bool))
if (value.kind != .boolean) return .invalid
if (value.boolean) selected = conditional.then_block
else if (conditional.else_block) |other| selected = other
else selected = try ast.add_block(state.builder, conditional.span, &[])
}
}
if (kind == .expression) {
expression :: ast.statement_expr(&state.tree, id)
if (ast.expr_kind(&state.tree, expression) == .match_expr) {
subject :: ast.match_expr(&state.tree, expression).subject
explicit :: ast.expr_kind(&state.tree, subject) == .comptime_expr
try expand(state, subject)
if (explicit or branch_constant(state, subject, 0)) {
index :: try type_match_arm(state, expression)
if (index == no_index) return .invalid
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index))
body :: try type_match_body(state, expression, arm)
if (body == no_node) return .invalid
if (!arm.expression) {
selected = body
value_source = true
} else {
statement :: try ast.add_expression_stmt(state.builder, body)
statements [1]ast.NodeId :: [statement]
selected = try ast.add_block(state.builder, ast.node_span(&state.tree, id), statements[..])
}
}
}
}
if (selected != no_node) {
try refresh(state)
flow := try inline_block(state, selected)
replacement := no_node
if (value_source) {
value :: try ast.add_block_expr(state.builder, ast.node_span(&state.tree, selected), selected)
replacement = try ast.add_expression_stmt(state.builder, value)
if (flow == .yielded) flow = .next
} else replacement = try ast.add_block_stmt(state.builder, selected)
try replace(state, id, replacement)
try refresh(state)
return flow
}
if (inline_has_transfer(state, id)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.inline_loop_control, ast.node_span(&state.tree, id))
return .invalid
}
try expand(state, id)
return .next
}
@hide:file
inline_block proc(state @mut State, block ast.NodeId) TypeFlow ! Error {
statements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&statements)
flow := TypeFlow.next
for (0..ast.block_statement_count(&state.tree, block)) |i| {
statement :: ast.block_statement(&state.tree, block, i)
if (!try type_step(state, statement)) return .invalid
flow = try inline_statement(state, statement)
if (flow == .invalid) return flow
kind :: ast.stmt_kind(&state.tree, statement)
if (kind != .break_stmt and kind != .continue_stmt) try arraylist.append(&statements, statement)
if (flow != .next) break
}
replacement :: try ast.add_block(state.builder, ast.node_span(&state.tree, block), statements.items)
try replace(state, block, replacement)
try refresh(state)
return flow
}
@hide:file
expand_inline_for proc(state @mut State, id ast.NodeId) void ! Error {
loop :: ast.for_stmt(&state.tree, id)
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
blocks := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&blocks)
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = ast.NodeId(0), result_type = no_node,
inline_body = loop.body, inline_blocks = &blocks,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
for (0..usize(2)) |i| {
capture :: if (i == 0) loop.capture else loop.index
if (capture) |node| {
resolved :: binding(state, node)
if (resolved.kind == .invalid) state.evaluation_diagnostic = resolved.data.diagnostic
}
}
if (loop.pointer_capture)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, loop.span)
flow := TypeFlow.invalid
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
flow = try execute_type_for(state, &execution, id)
if ((flow != .next and flow != .yielded and flow != .returned)
or state.evaluation_diagnostic != diagnostics.no_diagnostic) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_argument_required, loop.span)
invalid :: try ast.add_invalid_stmt(state.builder, loop.span, state.evaluation_diagnostic)
try replace(state, id, invalid)
try refresh(state)
return
}
statements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&statements)
for (blocks.items) |block| {
statement :: try ast.add_block_stmt(state.builder, block)
try arraylist.append(&statements, statement)
}
block :: try ast.add_block(state.builder, loop.span, statements.items)
replacement :: try ast.add_block_stmt(state.builder, block)
try replace(state, id, replacement)
try refresh(state)
}
# select with ordinary checked IR so patterns, overlap and exhaustiveness have
# the same rules as runtime matching, without checking unexecuted type bodies.
@hide:file
type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
try expand_inline_match(state, id, no_node)
if (ast.expr_kind(&state.tree, id) != .match_expr) return no_index
optional_capture :: ast.match_expr(&state.tree, id).optional_capture
if (!try expand_type_matches(state, ast.match_expr(&state.tree, id).subject)) return no_index
try expand(state, ast.match_expr(&state.tree, id).subject)
arms := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arms)
patterns := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&patterns)
subject :: ast.match_expr(&state.tree, id).subject
annotation :: try expression_type(state, subject, 0)
metadata :: type_record_storage(state, annotation, 0)
dispatch := subject
if (metadata) {
value :: try evaluate_expected(state, subject, annotation)
if (value.kind != .aggregate or ast.expr_kind(&state.tree, value.node) != .record_literal) return no_index
active :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0))
span :: ast.node_span(&state.tree, subject)
tags :: try metadata_tags(state, annotation)
tag :: try ast.add_enum_literal(state.builder, span, active.name?)
dispatch = try ast.add_typed_constant(state.builder, span, tags, tag)
try refresh(state)
}
for (0..ast.match_arm_count(&state.tree, id)) |i| {
arm_id :: ast.match_arm(&state.tree, id, i)
arm :: ast.match_arm_view(&state.tree, arm_id)
# selector bodies omit source capture uses, but must retain their resolver failures.
if (arm.capture) |capture| {
resolved :: binding(state, capture)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
return no_index
}
}
patterns.items = patterns.items[..0]
for (0..ast.match_pattern_count(&state.tree, arm_id)) |j| {
pattern :: ast.match_pattern(&state.tree, arm_id, j)
if (!try expand_type_matches(state, pattern)) return no_index
try expand(state, pattern)
try arraylist.append(&patterns, pattern)
}
if (metadata and arm.capture != null) {
expected := no_node
valid := !arm.catch_all and patterns.items.len > 0
for (patterns.items) |pattern| {
if (ast.expr_kind(&state.tree, pattern) != .enum_literal) {
valid = false
continue
}
slot :: type_record_slot(state, annotation, ast.enum_literal(&state.tree, pattern))
if (slot == no_index) {
valid = false
continue
}
payload :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot)).type?
if (binding(state, payload).kind == .builtin and binding(state, payload).data.builtin == .void)
valid = false
if (expected != no_node and !try same_type(state, expected, payload)) valid = false
expected = payload
}
if (!valid) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_match_capture, ast.node_span(&state.tree, arm_id))
return no_index
}
}
span :: ast.node_span(&state.tree, arm_id)
index :: try ast.add_integer(state.builder, span, u64(i))
capture :: if (metadata) null else arm.capture
selector :: try ast.add_match_arm(state.builder, span, patterns.items,
capture, index, true, arm.catch_all)
try refresh(state)
try arraylist.append(&arms, selector)
}
selector :: try ast.add_match(state.builder, ast.node_span(&state.tree, id),
dispatch, arms.items)
if (optional_capture)
ast.set_match_optional(state.builder, selector)
try refresh(state)
# selector arms contain only indices. do not specialize this synthetic match
# again while its checked evaluation expands the wrapper.
state.expanded.items[usize(selector)] = 1
selected :: try evaluate_ir(state, selector, try builtin(state, id, .usize))
if (selected.kind != .integer) return no_index
return usize(selected.magnitude)
}
# match checking has already validated every capture. copy the active payload
# out of owned constant storage, never an address in the temporary interpreter.
@hide:file
type_match_capture proc(state @mut State, id ast.NodeId) ast.NodeId ! Error {
subject :: ast.match_expr(&state.tree, id).subject
if (ast.match_expr(&state.tree, id).optional_capture) {
unwrapped :: try ast.add_unary(state.builder,
ast.node_span(&state.tree, subject), .unwrap, subject)
try refresh(state)
child :: try expression_type(state, unwrapped, 0)
return try type_execution_value(state, unwrapped, child)
}
annotation :: try expression_type(state, subject, 0)
value :: if (type_record_storage(state, annotation, 0))
try evaluate_expected(state, subject, annotation) else try evaluate_ir(state, subject, annotation)
if (value.kind != .aggregate or ast.expr_kind(&state.tree, value.node) != .record_literal) return no_node
payload :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0))
shape :: try inline_match_shape(state, annotation)
if (shape == no_node) return no_node
for (0..ast.record_field_count(&state.tree, shape)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, shape, i))
if (
field.name != null and payload.name != null
and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, payload.name?)
) {
if (
ast.expr_kind(&state.tree, payload.value?) == .typed_constant
and try same_type(state, field.type?, ast.typed_constant(&state.tree, payload.value?).type)
) return payload.value?
result :: try ast.add_typed_constant(state.builder,
ast.node_span(&state.tree, subject), field.type?, payload.value?)
try refresh(state)
return result
}
}
return no_node
}
@hide:file
type_match_body proc(state @mut State, id ast.NodeId, arm ast.MatchArmView) ast.NodeId ! Error {
if (arm.capture == null) return arm.body
value :: try type_match_capture(state, id)
if (value == no_node) return no_node
snapshot :: ast.typed_constant(&state.tree, value)
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = ast.NodeId(0),
result_type = no_node,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
captured :: if (is_type(state, snapshot.type)) snapshot.value else value
try arraylist.append(&execution.locals, TypeLocal{
declaration = arm.capture?, value = captured,
annotation = snapshot.type,
})
return try type_snapshot(state, &execution, arm.body)
}
@hide:file
type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Error {
if (input == no_node) return no_node
expression := input
while (ast.expr_kind(&state.tree, expression) == .match_expr) {
if (!try type_step(state, expression)) return no_node
index :: try type_match_arm(state, expression)
if (index == no_index) return no_node
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index))
expression = try type_match_body(state, expression, arm)
if (!arm.expression and expression != no_node) {
expression = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, expression), expression)
try refresh(state)
}
if (expression == no_node) return no_node
}
if (expression != no_node and ast.expr_kind(&state.tree, expression) == .block_expr) {
annotation :: try value_block_type(state, ast.block_expr(&state.tree, expression), 0)
if (annotation != no_node and (is_type(state, annotation) or type_record_storage(state, annotation, 0)))
return try evaluate_type_value_block(state, expression, annotation)
}
return expression
}
# normalize selected value sources before ordinary expansion can visit their bodies.
# nominal results enter here after their instance declaration is cached.
@hide:file
expand_type_matches proc(state @mut State, id ast.NodeId) bool ! Error {
if (id == no_node) return false
if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .match_expr) {
selected :: try type_value_expression(state, id)
if (selected == no_node) return false
resolved :: binding(state, selected)
try remember(state, id)
ast.replace_node_from_source(state.builder, id, selected)
try refresh(state)
state.bindings.items[usize(id)] = resolved
}
for (0..ast.child_count(&state.tree, id)) |i| {
if (!try expand_type_matches(state, ast.child(&state.tree, id, i))) return false
}
return try type_array_projection(state, id)
}
@hide:file
type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error {
if (ast.node_class(&state.tree, id) != .expression) return true
kind :: ast.expr_kind(&state.tree, id)
if (kind == .index or kind == .selection) {
base :: if (kind == .index) ast.index_expr(&state.tree, id).base else ast.selection_expr(&state.tree, id).base
base_binding :: binding(state, base)
if (base_binding.kind == .invalid) {
state.evaluation_diagnostic = base_binding.data.diagnostic
return false
}
if (ast.expr_kind(&state.tree, base) != .typed_constant) return true
array :: ast.typed_constant(&state.tree, base)
type :: try canonical_type(state, array.type)
record :: (type != no_node and type_record_storage(state, type, 0)
and (ast.expr_kind(&state.tree, type) == .record_type
or ast.expr_kind(&state.tree, type) == .union_type))
if (
!record and !try execution_array_storage(state, array.type)
and !try metadata_sequence(state, array.type)
) return true
selected := no_node
if (record) {
if (kind != .selection) return true
member :: ast.selection_expr(&state.tree, id).member
slot :: type_record_slot(state, type, member)
if (slot == no_index) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unknown_field, ast.node_span(&state.tree, member))
return false
}
if (ast.expr_kind(&state.tree, type) == .union_type) {
active :: ast.field(&state.tree, ast.record_field(&state.tree, array.value, 0))
if (active.name == null or ast.name_id(&state.tree, active.name?) != ast.name_id(&state.tree, member)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.inactive_union_field, ast.node_span(&state.tree, id))
return false
}
selected = active.value?
} else selected = ast.field(&state.tree, ast.record_field(&state.tree, array.value, slot)).value?
} else if (kind == .index) {
index :: try type_array_index(state, ast.index_expr(&state.tree, id).index,
ast.array_element_count(&state.tree, array.value))
if (index == no_index) return false
selected = ast.array_element(&state.tree, array.value, index)
if (ast.expr_kind(&state.tree, selected) == .literal_undefined) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.read_before_initialization, ast.node_span(&state.tree, id))
return false
}
if (!type_record_storage(state, array.type, 0)
and ast.expr_kind(&state.tree, selected) != .typed_constant) {
descriptor :: ast.array_type(&state.tree, try canonical_type(state, array.type))
selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id),
descriptor.element, selected)
try refresh(state)
}
} else {
member :: ast.selection_expr(&state.tree, id).member
if (!type_record_storage(state, array.type, 0)
and !mem.eql(ast.literal_text(&state.tree, member), "len")) return true
if (!mem.eql(ast.literal_text(&state.tree, member), "len")) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unknown_field, ast.node_span(&state.tree, member))
return false
}
count :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id),
u64(ast.array_element_count(&state.tree, array.value)))
selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id),
try builtin(state, id, .usize), count)
try refresh(state)
}
# copying a nominal definition into the projection would create a new
# identity on every read. retain its original node behind a type alias.
if (nominal_type_value(state, selected)) {
selected = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, id), selected, true)
try refresh(state)
}
resolved :: binding(state, selected)
try remember(state, id)
ast.replace_node_from_source(state.builder, id, selected)
try refresh(state)
state.bindings.items[usize(id)] = resolved
}
return true
}
# type-control arrays are persistent AST snapshots, including uninitialized leaves.
# only fully initialized ordinary values may enter checked runtime IR.
@hide:file
execution_array_storage proc(state @mut State, annotation ast.NodeId) bool ! Error {
current :: try canonical_type(state, annotation)
return current != no_node and ast.expr_kind(&state.tree, current) == .array_type
}
@hide:file
metadata_sequence proc(state @mut State, annotation ast.NodeId) bool ! Error {
type :: try canonical_type(state, annotation)
return (type != no_node and ast.expr_kind(&state.tree, type) == .slice_type
and type_record_storage(state, type, 0))
}
@hide:file
type_record_storage proc(state @State, annotation ast.NodeId, depth usize) bool {
return type_storage_annotation(&state.tree, state.bindings.items, annotation, depth)
}
@hide:file
type_record_slot proc(state @State, record, member ast.NodeId) usize {
if (ast.expr_kind(&state.tree, member) != .literal_int) {
for (0..ast.record_field_count(&state.tree, record)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, record, i))
if (field.name != null and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, member))
return i
}
return no_index
}
ordinal := usize(0)
for (ast.literal_text(&state.tree, member)) |byte| {
if (byte < '0' or byte > '9') return no_index
digit :: usize(byte - '0')
if (ordinal > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index
ordinal = ordinal * 10 + digit
}
if (ordinal >= ast.record_field_count(&state.tree, record)) return no_index
if (ast.field(&state.tree, ast.record_field(&state.tree, record, ordinal)).name != null) return no_index
return ordinal
}
@hide:file
type_union_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error {
if (ast.expr_kind(&state.tree, expression) == .typed_constant) {
value :: ast.typed_constant(&state.tree, expression)
if (try same_type(state, type, value.type)) return expression
}
name := no_node
input := no_node
kind :: ast.expr_kind(&state.tree, expression)
if (kind == .enum_literal) name = ast.enum_literal(&state.tree, expression)
else if (kind == .record_literal and ast.record_field_count(&state.tree, expression) == 1) {
field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, 0))
if (field.name) |member| name = member
if (field.value) |value| input = value
}
slot :: if (name != no_node) type_record_slot(state, type, name) else no_index
if (slot == no_index) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.union_initializer_count, ast.node_span(&state.tree, expression))
return no_node
}
field :: ast.field(&state.tree, ast.record_field(&state.tree, type, slot))
payload_type :: try canonical_type(state, field.type?)
empty :: (binding(state, payload_type).kind == .builtin
and binding(state, payload_type).data.builtin == .void)
span :: ast.node_span(&state.tree, expression)
if (input == no_node and empty) input = try ast.add_literal(state.builder, .void, span)
if (input == no_node) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .missing_field, span)
return no_node
}
try refresh(state)
value :: try type_execution_value(state, input, field.type?)
if (value == no_node) return no_node
member :: try ast.add_field(state.builder, span, field.name, null, value)
fields [1]ast.NodeId :: [member]
literal :: try ast.add_record_literal(state.builder, span, null, fields[..])
result :: try ast.add_typed_constant(state.builder, span, type, literal)
try refresh(state)
return result
}
# keep ordinary field/default diagnostics even though the enclosing metadata record
# has no runtime layout. type leaves are checked here; value leaves use the checker.
@hide:file
type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error {
if (is_type(state, annotation)) return true
type :: try canonical_type(state, annotation)
if (type == no_node) return false
if (ast.expr_kind(&state.tree, type) == .slice_type)
return try type_record_check(state, ast.slice_type(&state.tree, type).element)
if (ast.expr_kind(&state.tree, type) == .array_type) {
if (!try type_step(state, type)) return false
descriptor :: ast.array_type(&state.tree, type)
if (descriptor.count) |count| {
value :: try evaluate(state, count)
if (value.kind != .integer or value.negative) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_array_length, ast.node_span(&state.tree, count))
return false
}
}
return try type_record_check(state, descriptor.element)
}
for (0..ast.record_field_count(&state.tree, type)) |i| {
field_id :: ast.record_field(&state.tree, type, i)
field :: ast.field(&state.tree, field_id)
if (!try type_step(state, field_id)) return false
if (field.name != null and type_record_slot(state, type, field.name?) != i) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span)
return false
}
if (ast.expr_kind(&state.tree, type) == .union_type) {
payload :: try canonical_type(state, field.type?)
if (
payload != no_node and binding(state, payload).kind == .builtin
and binding(state, payload).data.builtin == .void
) continue
}
if (type_record_storage(state, field.type?, 0)) {
if (!try type_record_check(state, field.type?)) return false
if (field.value != null and try type_execution_value(state, field.value?, field.type?) == no_node) return false
} else {
fields [1]ast.NodeId :: [field_id]
record :: try ast.add_record_type(state.builder, field.span, fields[..])
constraint :: try ast.add_type_operand(state.builder, field.span, record, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
if (field.value != null and try type_execution_value(state, field.value?, field.type?) == no_node) return false
}
}
return true
}
# normalize complete record snapshots in declaration order, evaluating every leaf
# through its declared context. no record containing type values enters runtime IR.
@hide:file
type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error {
type :: try canonical_type(state, annotation)
if (ast.expr_kind(&state.tree, type) == .union_type)
return try type_union_value(state, expression, type)
if (ast.expr_kind(&state.tree, expression) == .typed_constant) {
actual :: ast.typed_constant(&state.tree, expression)
if (try same_type(state, type, actual.type)) return expression
} else if (ast.expr_kind(&state.tree, expression) == .record_literal) {
if (!try type_record_check(state, type)) return no_node
literal :: ast.record_literal(&state.tree, expression)
if (literal.type != null and !try same_type(state, type, literal.type?)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.type_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
count :: ast.record_field_count(&state.tree, type)
values :: try mem.alloc(ast.NodeId, state.allocator, count)
defer mem.free(state.allocator, values)
mem.set(values, no_node)
for (0..ast.record_field_count(&state.tree, expression)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, i))
slot := i
if (field.name) |name| slot = type_record_slot(state, type, name)
if (slot >= count or values[slot] != no_node) {
code :: if (slot < count) diagnostics.Code.duplicate_field
else if (field.name != null) diagnostics.Code.unknown_field else diagnostics.Code.tuple_arity_mismatch
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, field.span)
return no_node
}
values[slot] = field.value?
}
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
for (0..count) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i))
if (field.name) |name| {
if (type_record_slot(state, type, name) != i) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span)
return no_node
}
}
input := values[i]
if (input == no_node and field.value != null) input = field.value?
if (input == no_node) {
code :: if (field.name == null) diagnostics.Code.tuple_arity_mismatch else diagnostics.Code.missing_field
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, expression))
return no_node
}
if (!try type_step(state, input)) return no_node
value :: try type_execution_value(state, input, field.type?)
if (value == no_node) return no_node
generated :: try ast.add_field(state.builder, field.span, field.name, null, value)
try refresh(state)
try arraylist.append(&fields, generated)
}
span :: ast.node_span(&state.tree, expression)
normalized :: try ast.add_record_literal(state.builder, span, null, fields.items)
result :: try ast.add_typed_constant(state.builder, span, type, normalized)
try refresh(state)
return result
}
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.type_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
@hide:file
type_array_index proc(state @mut State, expression ast.NodeId, count usize) usize ! Error {
usize_type :: try builtin(state, expression, .usize)
actual :: try expression_type(state, expression, 0)
if (!flexible(state, expression) and (actual == no_node or !try same_type(state, actual, usize_type))) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_index_type, ast.node_span(&state.tree, expression))
return no_index
}
value :: try evaluate_ir(state, expression, usize_type)
if (value.kind != .integer) return no_index
if (value.magnitude >= u64(count)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.index_out_of_bounds, ast.node_span(&state.tree, expression))
return no_index
}
return usize(value.magnitude)
}
@hide:file
type_array_compatible proc(state @mut State, expected, actual ast.NodeId) bool ! Error {
if (is_type(state, expected) or is_type(state, actual))
return is_type(state, expected) and is_type(state, actual)
left :: try canonical_type(state, expected)
right :: try canonical_type(state, actual)
if (left == no_node or right == no_node) return false
if (ast.expr_kind(&state.tree, left) != .array_type or ast.expr_kind(&state.tree, right) != .array_type)
return try same_type(state, left, right)
a :: ast.array_type(&state.tree, left)
b :: ast.array_type(&state.tree, right)
if (a.mutable != b.mutable or b.count == null) return false
if (a.count) |count| {
if (!try value_equal(state, try evaluate(state, count), try evaluate(state, b.count?))) return false
}
return try type_array_compatible(state, a.element, b.element)
}
# undefined leaves are initialization state, not type values. snapshots may copy
# that state locally; procedure boundaries require every leaf to be initialized.
@hide:file
type_array_initialized proc(state @mut State, value, at ast.NodeId) bool ! Error {
kind :: ast.expr_kind(&state.tree, value)
if (kind == .literal_undefined) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.read_before_initialization, ast.node_span(&state.tree, at))
return false
}
if (kind == .typed_constant)
return try type_array_initialized(state, ast.typed_constant(&state.tree, value).value, at)
if (kind == .array_literal) {
for (0..ast.array_element_count(&state.tree, value)) |i| {
if (!try type_array_initialized(state, ast.array_element(&state.tree, value, i), at)) return false
}
}
if (kind == .record_literal) {
for (0..ast.record_field_count(&state.tree, value)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, value, i))
if (field.value != null and !try type_array_initialized(state, field.value?, at)) return false
}
}
return true
}
# bound the expanded snapshot, not just its dimensions, so later reads and
# initialization checks cannot traverse an exponentially shared empty shape.
@hide:file
type_array_undefined proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error {
if (is_type(state, annotation)) return expression
type :: try canonical_type(state, annotation)
if (type == no_node or ast.expr_kind(&state.tree, type) != .array_type) return expression
descriptor :: ast.array_type(&state.tree, type)
length := Value{}
if (descriptor.count) |count| length = try evaluate(state, count)
if (length.kind != .integer or length.negative) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_array_length, ast.node_span(&state.tree, annotation))
return no_node
}
# validate child dimensions even when this dimension is empty.
child :: try type_array_undefined(state, expression, descriptor.element)
if (child == no_node) return no_node
elements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&elements)
for (0..length.magnitude) |i| {
if (!try type_step(state, expression)) return no_node
element := child
if (i != 0) element = try type_array_undefined(state, expression, descriptor.element)
if (element == no_node) return no_node
try arraylist.append(&elements, element)
}
span :: ast.node_span(&state.tree, expression)
type_span :: ast.node_span(&state.tree, annotation)
element_type :: if (ast.expr_kind(&state.tree, child) == .typed_constant)
ast.typed_constant(&state.tree, child).type else descriptor.element
count :: try ast.add_integer(state.builder, span, length.magnitude)
concrete :: try ast.add_array_type(state.builder, type_span, count, element_type, descriptor.mutable)
literal :: try ast.add_array_literal(state.builder, span, elements.items)
result :: try ast.add_typed_constant(state.builder, span, concrete, literal)
try refresh(state)
return result
}
@hide:file
type_array_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error {
if (ast.expr_kind(&state.tree, expression) == .literal_undefined)
return try type_array_undefined(state, expression, annotation)
type :: try canonical_type(state, annotation)
array :: ast.array_type(&state.tree, type)
if (ast.expr_kind(&state.tree, expression) == .typed_constant) {
actual :: ast.typed_constant(&state.tree, expression)
if (try type_array_compatible(state, type, actual.type)) return expression
if (!try type_array_initialized(state, expression, expression)) return no_node
} else if (ast.expr_kind(&state.tree, expression) == .array_literal) {
count :: ast.array_element_count(&state.tree, expression)
if (array.count) |length| {
value :: try evaluate(state, length)
if (value.kind != .integer or value.negative) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_array_length, ast.node_span(&state.tree, length))
return no_node
}
if (value.magnitude != u64(count)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.array_length_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
}
elements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&elements)
element_type := array.element
for (0..count) |i| {
element :: try type_execution_value(state,
ast.array_element(&state.tree, expression, i), element_type)
if (element == no_node) return no_node
if (ast.expr_kind(&state.tree, element) == .typed_constant)
element_type = ast.typed_constant(&state.tree, element).type
try arraylist.append(&elements, element)
}
# empty outer arrays still need concrete, valid child dimensions.
if (count == 0) {
child := element_type
while (!is_type(state, child)) {
child = try canonical_type(state, child)
if (child == no_node or ast.expr_kind(&state.tree, child) != .array_type) break
descriptor :: ast.array_type(&state.tree, child)
value := Value{}
if (descriptor.count) |length| value = try evaluate(state, length)
if (value.kind != .integer or value.negative) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_array_length, ast.node_span(&state.tree, child))
return no_node
}
child = descriptor.element
}
if (type_record_storage(state, child, 0) and !try type_record_check(state, child)) return no_node
}
span :: ast.node_span(&state.tree, expression)
length :: try ast.add_integer(state.builder, span, u64(count))
concrete :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, annotation),
length, element_type, array.mutable)
literal :: try ast.add_array_literal(state.builder, span, elements.items)
result :: try ast.add_typed_constant(state.builder, span, concrete, literal)
try refresh(state)
return result
}
if (type_record_storage(state, annotation, 0)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.type_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
value :: try evaluate_ir(state, expression, annotation)
if (value.kind == .invalid) return no_node
result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, value.node)
try refresh(state)
return result
}
# rebuild only the field/index path so previous snapshots retain their owned values.
@hide:file
type_array_update proc(
state @mut State, execution @mut TypeExecution, target, root, current, input ast.NodeId,
) ast.NodeId ! Error {
# find the outermost projection; rebuild its child before copying this parent.
first := target
while (true) {
kind :: ast.expr_kind(&state.tree, first)
if (kind != .index and kind != .selection) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_assignment_target, ast.node_span(&state.tree, target))
return no_node
}
base :: if (kind == .index) ast.index_expr(&state.tree, first).base
else ast.selection_expr(&state.tree, first).base
if (base == root) break
first = base
}
array :: ast.typed_constant(&state.tree, current)
type :: try canonical_type(state, array.type)
if (
type == no_node
or (ast.expr_kind(&state.tree, type) != .record_type and ast.expr_kind(&state.tree, type) != .array_type)
) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_index_assignment, ast.node_span(&state.tree, target))
return no_node
}
record :: ast.expr_kind(&state.tree, type) == .record_type
index := no_index
element_type := no_node
count := usize(0)
previous := no_node
if (record) {
if (ast.expr_kind(&state.tree, first) == .selection)
index = type_record_slot(state, type, ast.selection_expr(&state.tree, first).member)
if (index == no_index) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unknown_field, ast.node_span(&state.tree, first))
return no_node
}
element_type = ast.field(&state.tree, ast.record_field(&state.tree, type, index)).type?
count = ast.record_field_count(&state.tree, array.value)
previous = ast.field(&state.tree, ast.record_field(&state.tree, array.value, index)).value?
} else {
descriptor :: ast.array_type(&state.tree, type)
if (!descriptor.mutable or ast.expr_kind(&state.tree, first) != .index) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_index_assignment, ast.node_span(&state.tree, target))
return no_node
}
expression :: try type_snapshot(state, execution, ast.index_expr(&state.tree, first).index)
if (!try expand_type_matches(state, expression)) return no_node
count = ast.array_element_count(&state.tree, array.value)
index = try type_array_index(state, expression, count)
if (index == no_index) return no_node
element_type = descriptor.element
previous = ast.array_element(&state.tree, array.value, index)
}
updated := no_node
if (first == target) updated = try type_execution_value(state, input, element_type)
else {
if (ast.expr_kind(&state.tree, previous) != .typed_constant) {
previous = try type_execution_value(state, previous, element_type)
if (previous == no_node) return no_node
}
updated = try type_array_update(state, execution, target, first, previous, input)
}
if (updated == no_node) return no_node
elements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&elements)
for (0..count) |i| {
element := no_node
if (record) {
field :: ast.field(&state.tree, ast.record_field(&state.tree, array.value, i))
value :: if (i == index) updated else field.value?
element = try ast.add_field(state.builder, field.span, field.name, null, value)
try refresh(state)
} else element = if (i == index) updated else ast.array_element(&state.tree, array.value, i)
try arraylist.append(&elements, element)
}
span :: ast.node_span(&state.tree, target)
literal := no_node
if (record) literal = try ast.add_record_literal(state.builder, span, null, elements.items)
else literal = try ast.add_array_literal(state.builder, span, elements.items)
result :: try ast.add_typed_constant(state.builder, span, array.type, literal)
try refresh(state)
return result
}
@hide:file
type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error {
if (input == no_node or annotation == no_node) return no_node
if (ast.expr_kind(&state.tree, input) == .block_expr
and (is_type(state, annotation) or type_record_storage(state, annotation, 0)))
return try evaluate_type_value_block(state, input, annotation)
expression :: try type_value_expression(state, input)
if (expression == no_node) return no_node
if (!try expand_type_matches(state, annotation)) return no_node
# a recursive record needs its cached declaration before expanding its fields.
if (is_type(state, annotation) and nominal_type_value(state, expression)) {
concrete :: try materialize(state, Value{ kind = .type, node = expression })
if (type_record_storage(state, expression, 0)) {
if (!try type_record_check(state, expression)) return no_node
return concrete.node
}
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), concrete.node, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
return concrete.node
}
if (!try expand_type_matches(state, expression)) return no_node
try expand_expected(state, expression, annotation)
resolved :: binding(state, expression)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
return no_node
}
if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)
and resolved.kind == .declaration
and ast.node_class(&state.tree, resolved.data.declaration) == .statement) {
declaration :: ast.statement_decl(&state.tree, resolved.data.declaration)
if (declaration.mutability != .constant or !try type_step(state, expression)) return no_node
if (declaration.type != null and !try same_type(state, annotation, declaration.type?)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.type_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
return try type_execution_value(state, declaration.initializer, annotation)
}
if (try execution_array_storage(state, annotation)) {
value :: try type_array_value(state, expression, annotation)
if (value != no_node and !type_record_storage(state, annotation, 0)) {
# erased snapshots still owe ordinary array-element/layout checking.
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression),
ast.typed_constant(&state.tree, value).type, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
}
return value
}
if (
!is_type(state, annotation) and type_record_storage(state, annotation, 0)
and ast.expr_kind(&state.tree, try canonical_type(state, annotation)) == .slice_type
) {
if (ast.expr_kind(&state.tree, expression) == .typed_constant) {
actual :: ast.typed_constant(&state.tree, expression)
if (try same_type(state, annotation, actual.type)) return expression
}
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.type_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
if (!is_type(state, annotation) and type_record_storage(state, annotation, 0))
return try type_record_value(state, expression, annotation)
if (is_type(state, annotation)) {
if (ast.expr_kind(&state.tree, expression) == .literal_undefined) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.read_before_initialization, ast.node_span(&state.tree, expression))
return no_node
}
value :: try evaluate(state, expression)
if (value.kind != .type) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.type_mismatch, ast.node_span(&state.tree, expression))
return no_node
}
concrete :: try materialize(state, value)
if (type_record_storage(state, concrete.node, 0)) {
if (!try type_record_check(state, concrete.node)) return no_node
return concrete.node
}
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), concrete.node, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
return concrete.node
}
kind :: ast.expr_kind(&state.tree, expression)
if (kind == .literal_string or kind == .typed_constant) {
value :: try evaluate(state, expression)
if (
value.kind == .string and try valid_value(state, annotation, value)
and (value.annotation == no_node or try same_type(state, annotation, value.annotation))
) {
result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression),
annotation, value.node)
try refresh(state)
return result
}
}
# check the leaf as an assignment, not as the evaluation wrapper's return:
# exact value-type failures belong to the source leaf and use type_mismatch.
contextual :: try ast.add_typed_constant(state.builder,
ast.node_span(&state.tree, expression), annotation, expression)
try refresh(state)
value :: try evaluate_ir(state, contextual, annotation)
if (value.kind == .invalid) return no_node
concrete :: try materialize(state, value)
result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, concrete.node)
try refresh(state)
return result
}
# check capture guards with ordinary IR without elaborating either source body.
@hide:file
type_capture_guard proc(state @mut State, expression, capture ast.NodeId) Value ! Error {
resolved :: binding(state, capture)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
return Value{}
}
span :: ast.node_span(&state.tree, expression)
yes :: try ast.add_boolean(state.builder, span, true)
no :: try ast.add_boolean(state.builder, span, false)
taken :: try ast.add_return_stmt(state.builder, span, yes)
skipped :: try ast.add_return_stmt(state.builder, span, no)
taken_statements [1]ast.NodeId :: [taken]
skipped_statements [1]ast.NodeId :: [skipped]
then_block :: try ast.add_block(state.builder, span, taken_statements[..])
else_block :: try ast.add_block(state.builder, span, skipped_statements[..])
selector :: try ast.add_if_stmt(state.builder, span, expression, then_block, else_block)
ast.set_capture(state.builder, selector, capture)
statements [1]ast.NodeId :: [selector]
body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
return try evaluate_ir_body(state, expression, body, try builtin(state, expression, .bool))
}
# update one owned aggregate through ordinary assignment checking and execution.
# only the target's root becomes a temporary local; indices and RHS retain their
# pre-assignment snapshots, and no interpreter storage survives the evaluation.
@hide:file
type_assignment_value proc(
state @mut State, execution @mut TypeExecution, statement ast.NodeId, index usize,
) ast.NodeId ! Error {
assignment :: ast.assignment_stmt(&state.tree, statement)
local :: execution.locals.items[index]
span :: ast.node_span(&state.tree, statement)
value :: try type_snapshot(state, execution, assignment.value)
if (value == no_node) return no_node
mutability :: if (ast.node_class(&state.tree, local.declaration) == .statement)
ast.statement_decl(&state.tree, local.declaration).mutability else ast.Mutability.constant
temporary :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str,
local.annotation, mutability, local.value)
try refresh(state)
target := no_node
{
execution.locals.items[index].value = try reference(state, assignment.target, temporary)
defer execution.locals.items[index].value = local.value
target = try type_snapshot(state, execution, assignment.target)
}
if (!try expand_type_matches(state, target) or !try expand_type_matches(state, value)) return no_node
update :: try ast.add_assignment_stmt(state.builder, span, target, value)
returned :: try ast.add_return_stmt(state.builder, span, try reference(state, statement, temporary))
statements [3]ast.NodeId :: [temporary, update, returned]
body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
result :: try evaluate_ir_body(state, statement, body, local.annotation)
if (result.kind == .invalid) return no_node
concrete :: try materialize(state, result)
owned :: try ast.add_typed_constant(state.builder, span, local.annotation, concrete.node)
try refresh(state)
return owned
}
@hide:file
execute_type_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId) TypeFlow ! Error {
if (block == execution.inline_body) {
snapshot :: try type_snapshot(state, execution, block)
if (snapshot == no_node) return .invalid
flow :: try inline_block(state, snapshot)
if (flow == .invalid) return flow
try arraylist.append(execution.inline_blocks?, snapshot)
return flow
}
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
reached := usize(0)
flow := TypeFlow.next
for (0..ast.block_statement_count(&state.tree, block)) |i| {
statement :: ast.block_statement(&state.tree, block, i)
if (!try type_step(state, statement)) return .invalid
reached = i + 1
flow = try execute_type_statement(state, execution, statement)
if (flow != .next) break
}
if (flow == .invalid) return flow
# yielded/returned values already own their snapshots before scope cleanup runs.
while (reached > 0) {
reached -= 1
statement :: ast.block_statement(&state.tree, block, reached)
if (ast.stmt_kind(&state.tree, statement) != .defer_stmt) continue
saved_depth :: execution.cleanup_yield_depth
saved_loop :: execution.cleanup_loop_depth
execution.cleanup_loop_depth = execution.loop_depth
execution.cleanup_yield_depth = execution.yield_depth
execution.cleanup_depth += 1
cleanup :: try execute_type_block(state, execution, ast.cleanup_stmt(&state.tree, statement).body)
execution.cleanup_depth -= 1
execution.cleanup_yield_depth = saved_depth
execution.cleanup_loop_depth = saved_loop
if (cleanup != .next) return .invalid
}
return flow
}
# substitute selector names without snapshotting away the assignment's root place.
@hide:file
type_assignment_target proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .builtin_call) {
name :: ast.builtin_call(&state.tree, id).name
if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) return id
if (ast.call_arg_count(&state.tree, id) != 2) {
try reject(state, id, .argument_count_mismatch)
return no_node
}
base :: try type_assignment_target(state, execution, ast.call_arg(&state.tree, id, 0))
if (base == no_node) return no_node
argument :: try type_snapshot(state, execution, ast.call_arg(&state.tree, id, 1))
if (argument == no_node) return no_node
arguments [2]ast.NodeId :: [base, argument]
result :: try ast.add_builtin_call(state.builder, ast.node_span(&state.tree, id), name, arguments[..])
try refresh(state)
try expand(state, result)
if (binding(state, result).kind == .invalid) return no_node
return result
}
if (kind != .selection and kind != .index) return id
base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base
selected :: try type_assignment_target(state, execution, base)
if (selected == no_node or selected == base) return if (selected == no_node) no_node else id
result := no_node
if (kind == .selection)
result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id),
selected, ast.selection_expr(&state.tree, id).member)
else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id),
selected, ast.index_expr(&state.tree, id).index)
try refresh(state)
return result
}
@hide:file
execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
if (execution.cleanup_depth > 0) {
kind :: ast.stmt_kind(&state.tree, statement)
if (kind == .return_stmt
or ((kind == .break_stmt or kind == .continue_stmt) and execution.loop_depth <= execution.cleanup_loop_depth)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.cleanup_control_flow, ast.node_span(&state.tree, statement))
return .invalid
}
}
flow :: try execute_type_statement_body(state, execution, statement)
if (execution.pending_flow == .next) return flow
pending :: execution.pending_flow
execution.pending_flow = .next
return pending
}
@hide:file
execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
match ast.stmt_kind(&state.tree, statement) {
.unreachable_stmt: {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unreachable_executed, ast.node_span(&state.tree, statement))
return .invalid
}
.defer_stmt: {}
.errdefer_stmt: {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.errdefer_outside_fallible, ast.node_span(&state.tree, statement))
return .invalid
}
.yield_stmt: {
if (execution.yield_depth == 0) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.yield_outside_block, ast.node_span(&state.tree, statement))
return .invalid
}
if (execution.cleanup_depth > 0 and execution.yield_depth <= execution.cleanup_yield_depth) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.cleanup_control_flow, ast.node_span(&state.tree, statement))
return .invalid
}
value :: ast.yield_stmt(&state.tree, statement).value?
expression :: try type_snapshot_expected(state, execution, value, execution.yield_type)
if (expression == no_node) return .invalid
annotation := execution.yield_type
if (annotation == no_node) annotation = try expression_type(state, expression, 0)
execution.yielded = try type_execution_value(state, expression, annotation)
if (execution.yielded == no_node
or !try type_array_initialized(state, execution.yielded, value)) return .invalid
return .yielded
}
.return_stmt: {
if (ast.return_stmt(&state.tree, statement).value) |value| {
expression :: try type_snapshot_expected(state, execution, value, execution.result_type)
if (expression == no_node) return .invalid
execution.returned = try type_execution_value(state, expression, execution.result_type)
if (execution.returned != no_node
and !try type_array_initialized(state, execution.returned, value)) return .invalid
if (execution.returned != no_node) return .returned
}
return .invalid
}
.declaration: {
declaration :: ast.statement_decl(&state.tree, statement)
annotation := no_node
if (declaration.type) |type| annotation = try type_snapshot(state, execution, type)
snapshot :: try type_snapshot_expected(state, execution, declaration.initializer, annotation)
expression :: try type_value_expression(state, snapshot)
if (expression == no_node) return .invalid
if (annotation == no_node and nominal_type_value(state, expression))
annotation = try builtin(state, expression, .type)
else if (annotation == no_node) {
if (!try expand_type_matches(state, expression)) return .invalid
try expand(state, expression)
annotation = try expression_type(state, expression, 0)
if (annotation == no_node) {
value :: try evaluate(state, expression)
if (value.kind == .type) annotation = try builtin(state, expression, .type)
}
}
if (annotation == no_node) return .invalid
value :: try type_execution_value(state, expression, annotation)
if (value == no_node) return .invalid
if (ast.expr_kind(&state.tree, value) == .typed_constant)
annotation = ast.typed_constant(&state.tree, value).type
try arraylist.append(&execution.locals, TypeLocal{
declaration = statement, value = value, annotation = annotation,
})
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(&state.tree, statement)
target :: try type_assignment_target(state, execution, assignment.target)
if (target == no_node) return .invalid
if (target != assignment.target) {
normalized :: try ast.add_assignment_stmt(state.builder,
ast.node_span(&state.tree, statement), target, assignment.value)
if (assignment.compound) ast.set_assignment_compound(state.builder, normalized)
try refresh(state)
# synthesized places still need local substitution in snapshots.
previous :: execution.mapping.len
execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree))
mem.set(execution.mapping[previous..], no_node)
return try execute_type_statement(state, execution, normalized)
}
root := assignment.target
indexed := true
while (true) {
kind :: ast.expr_kind(&state.tree, root)
if (kind == .selection and binding(state, root).kind == .none) {
indexed = false
root = ast.selection_expr(&state.tree, root).base
}
else if (kind == .index) root = ast.index_expr(&state.tree, root).base
else break
}
resolved :: binding(state, root)
index := no_index
if (resolved.kind == .declaration) {
for (execution.locals.items) |local, i| {
if (local.declaration == resolved.data.declaration) index = i
}
}
if (
index == no_index
or (root == assignment.target
and (ast.node_class(&state.tree, execution.locals.items[index].declaration) != .statement
or ast.statement_decl(&state.tree, execution.locals.items[index].declaration).mutability != .variable))
) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_assignment_target, ast.node_span(&state.tree, assignment.target))
return .invalid
}
if (root != assignment.target) {
if ((indexed and try execution_array_storage(state, execution.locals.items[index].annotation))
or type_record_storage(state, execution.locals.items[index].annotation, 0)) {
local :: execution.locals.items[index]
if (
ast.node_class(&state.tree, local.declaration) != .statement
or ast.statement_decl(&state.tree, local.declaration).mutability != .variable
) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_index_assignment, ast.node_span(&state.tree, assignment.target))
return .invalid
}
input :: try type_snapshot(state, execution, assignment.value)
if (!try expand_type_matches(state, input)) return .invalid
value :: try type_array_update(state, execution, assignment.target, root, local.value, input)
if (value == no_node) return .invalid
execution.locals.items[index].value = value
return .next
}
value :: try type_assignment_value(state, execution, statement, index)
if (value == no_node) return .invalid
execution.locals.items[index].value = value
return .next
}
expression :: try type_snapshot_expected(state, execution,
assignment.value, execution.locals.items[index].annotation)
value :: try type_execution_value(state, expression, execution.locals.items[index].annotation)
if (value == no_node) return .invalid
execution.locals.items[index].value = value
}
.if_stmt, .while_stmt: {
looping :: ast.stmt_kind(&state.tree, statement) == .while_stmt
condition := no_node
body := no_node
other := no_node
capture ?ast.NodeId := null
if (looping) {
view :: ast.while_stmt(&state.tree, statement)
capture = view.capture
condition = view.condition
body = view.body
} else {
view :: ast.if_stmt(&state.tree, statement)
capture = view.capture
condition = view.condition
body = view.then_block
if (view.else_block) |branch| other = branch
}
if (looping) execution.loop_depth += 1
defer if (looping) execution.loop_depth -= 1
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
invariant :: branch_constant(state, condition, 0)
value := Value{}
while (true) {
if (!try type_step(state, condition)) return .invalid
if (!invariant or value.kind == .invalid) {
expression :: try type_snapshot(state, execution, condition)
if (expression == no_node) return .invalid
if (capture) |name| value = try type_capture_guard(state, expression, name)
else {
boolean :: try builtin(state, expression, .bool)
value = try evaluate_ir(state, expression, boolean)
}
}
if (value.kind != .boolean) return .invalid
if (!value.boolean) {
if (!looping and other != no_node) return try execute_type_block(state, execution, other)
return .next
}
execution.locals.items = execution.locals.items[..saved]
if (capture) |name| {
expression :: try type_snapshot(state, execution, condition)
if (expression == no_node) return .invalid
unwrapped :: try ast.add_unary(state.builder,
ast.node_span(&state.tree, condition), .unwrap, expression)
try refresh(state)
annotation :: try expression_type(state, unwrapped, 0)
payload :: try type_execution_value(state, unwrapped, annotation)
if (payload == no_node) return .invalid
try arraylist.append(&execution.locals, TypeLocal{
declaration = name, value = payload, annotation = annotation,
})
}
flow :: try execute_type_block(state, execution, body)
if (!looping or flow == .returned or flow == .yielded or flow == .invalid) return flow
if (flow == .break) return .next
}
}
.expression: {
expression :: ast.statement_expr(&state.tree, statement)
if (ast.expr_kind(&state.tree, expression) != .match_expr) {
snapshot :: try type_snapshot(state, execution, expression)
if (snapshot == no_node) return .invalid
annotation :: try expression_type(state, snapshot, 0)
if (try type_execution_value(state, snapshot, annotation) == no_node) return .invalid
return .next
}
subject_type := no_node
arms :: ast.match_arm_count(&state.tree, expression)
if (arms > 0
and ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, arms - 1)).is_inline) {
subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, expression).subject)
if (subject == no_node) return .invalid
subject_type = try expression_type(state, subject, 0)
}
before :: ast.node_count(&state.tree)
try expand_inline_match(state, expression, subject_type)
if (ast.expr_kind(&state.tree, expression) != .match_expr) return .invalid
if (ast.node_count(&state.tree) != before) {
previous :: execution.mapping.len
execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree))
mem.set(execution.mapping[previous..], no_node)
}
snapshot :: try type_dispatch_snapshot(state, execution, expression)
if (snapshot == no_node) return .invalid
index :: try type_match_arm(state, snapshot)
if (index == no_index) return .invalid
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index))
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
if (arm.capture) |capture| {
value :: try type_match_capture(state, snapshot)
if (value == no_node) return .invalid
constant :: ast.typed_constant(&state.tree, value)
captured :: if (is_type(state, constant.type)) constant.value else value
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = captured,
annotation = constant.type,
})
}
if (!arm.expression) {
if (try execute_type_value_block(state, execution, arm.body, no_node) == no_node) return .invalid
return .next
}
body :: try type_snapshot(state, execution, arm.body)
value :: try type_value_expression(state, body)
if (value == no_node) return .invalid
annotation :: try expression_type(state, value, 0)
if (try type_execution_value(state, value, annotation) == no_node) return .invalid
}
.for_stmt: return try execute_type_for(state, execution, statement)
.block_stmt: return try execute_type_block(state, execution, ast.statement_block(&state.tree, statement))
.break_stmt, .continue_stmt: {
if (execution.loop_depth == 0) {
code :: if (ast.stmt_kind(&state.tree, statement) == .break_stmt)
diagnostics.Code.break_outside_loop else diagnostics.Code.continue_outside_loop
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, statement))
return .invalid
}
return if (ast.stmt_kind(&state.tree, statement) == .break_stmt) TypeFlow.break else TypeFlow.continue
}
else: return .invalid
}
return .next
}
# retain a collection place, but freeze its indices at loop entry. snapshots then read
# the current owned root after body assignments without re-evaluating the place.
@hide:file
type_collection_place proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
for (execution.locals.items) |local| {
if (local.declaration == resolved.data.declaration) return id
}
return no_node
}
kind :: ast.expr_kind(&state.tree, id)
if (kind != .selection and kind != .index) return no_node
base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base
place :: try type_collection_place(state, execution, base)
if (place == no_node) return no_node
result := no_node
if (kind == .selection) {
result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id),
place, ast.selection_expr(&state.tree, id).member)
} else {
index :: try type_snapshot(state, execution, ast.index_expr(&state.tree, id).index)
if (index == no_node) return no_node
value :: try type_execution_value(state, index, try builtin(state, index, .usize))
if (value == no_node) return no_node
result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), place, value)
}
try refresh(state)
return result
}
@hide:file
type_collection_read proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
if (binding(state, id).kind == .declaration) return try type_snapshot(state, execution, id)
kind :: ast.expr_kind(&state.tree, id)
base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base
value :: try type_collection_read(state, execution, base)
result := no_node
if (kind == .selection)
result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id),
value, ast.selection_expr(&state.tree, id).member)
else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id),
value, ast.index_expr(&state.tree, id).index)
try refresh(state)
return result
}
@hide:file
execute_type_collection proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
loop :: ast.for_stmt(&state.tree, statement)
span :: ast.node_span(&state.tree, statement)
if (loop.capture) |capture| {
if (binding(state, capture).kind == .invalid) {
state.evaluation_diagnostic = binding(state, capture).data.diagnostic
return .invalid
}
}
if (loop.index) |index| {
if (binding(state, index).kind == .invalid) {
state.evaluation_diagnostic = binding(state, index).data.diagnostic
return .invalid
}
}
snapshot :: try type_snapshot(state, execution, loop.iterable)
if (!try expand_type_matches(state, snapshot)) return .invalid
try expand(state, snapshot)
annotation :: try expression_type(state, snapshot, 0)
if (annotation == no_node) return .invalid
tuple :: loop.is_inline and ast.expr_kind(&state.tree, annotation) == .record_type
if tuple {
for (0..ast.record_field_count(&state.tree, annotation)) |i| {
if (ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).name != null) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_for_iterable, span)
return .invalid
}
}
}
collection :: try type_execution_value(state, snapshot, annotation)
if (collection == no_node) return .invalid
count := Value{}
if tuple {
count = Value{ kind = .integer, magnitude = u64(ast.record_field_count(&state.tree, annotation)) }
} else if (try execution_array_storage(state, annotation) or try metadata_sequence(state, annotation)) {
count = Value{ kind = .integer,
magnitude = u64(ast.array_element_count(&state.tree, ast.typed_constant(&state.tree, collection).value)) }
} else {
# validate the loop contract even for empty collections, without executing
# its type-valued body. the synthetic body exits before reading an element.
stop :: try ast.add_break_stmt(state.builder, span)
stops [1]ast.NodeId :: [stop]
body :: try ast.add_block(state.builder, span, stops[..])
selector :: try ast.add_for_stmt(state.builder, span, collection, loop.capture, loop.index,
loop.pointer_capture, loop.index_capture, body)
try refresh(state)
length := no_node
if (ast.expr_kind(&state.tree, annotation) == .array_type) length = ast.array_type(&state.tree, annotation).count?
else {
value :: try evaluate(state, collection)
if (value.kind == .string) {
text :: ast.literal_text(&state.tree, value.node)
cursor := usize(1)
size := u64(0)
while (cursor < text.len - 1) {
_ = ast.string_next(text, &cursor) catch { return .invalid }
size += 1
}
length = try ast.add_integer(state.builder, span, size)
} else length = try ast.add_integer(state.builder, span, 0)
}
returned :: try ast.add_return_stmt(state.builder, span, length)
statements [2]ast.NodeId :: [selector, returned]
wrapper :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
count = try evaluate_ir_body(state, statement, wrapper, try builtin(state, statement, .usize))
if (count.kind != .integer) return .invalid
}
if (loop.pointer_capture and loop.capture != null) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
return .invalid
}
place := no_node
if (tuple or ast.expr_kind(&state.tree, annotation) == .array_type)
place = try type_collection_place(state, execution, loop.iterable)
if (state.evaluation_diagnostic != diagnostics.no_diagnostic) return .invalid
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
execution.loop_depth += 1
defer execution.loop_depth -= 1
for (0..usize(count.magnitude)) |i| {
if (!try type_step(state, statement)) return .invalid
execution.locals.items = execution.locals.items[..saved]
index :: try ast.add_integer(state.builder, span, u64(i))
try refresh(state)
if loop.capture |capture| {
current := collection
if (place != no_node) current = try type_collection_read(state, execution, place)
element :: if (tuple) try ast.add_selection(state.builder, span, current, index)
else try ast.add_index(state.builder, span, current, index)
try refresh(state)
element_type :: if (tuple)
ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).type?
else try expression_type(state, element, 0)
value :: try type_execution_value(state, element, element_type)
if (value == no_node) return .invalid
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = value, annotation = element_type,
})
}
if (loop.index) |capture| {
type :: try builtin(state, capture, .usize)
value :: try type_execution_value(state, index, type)
if (value == no_node) return .invalid
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = value, annotation = type,
})
}
flow :: try execute_type_block(state, execution, loop.body)
if (flow == .returned or flow == .yielded or flow == .invalid) return flow
if (flow == .break) return .next
}
return .next
}
@hide:file
execute_type_for proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
loop :: ast.for_stmt(&state.tree, statement)
if (ast.expr_kind(&state.tree, loop.iterable) != .range)
return try execute_type_collection(state, execution, statement)
if (loop.pointer_capture or loop.index_capture) return .invalid
bounds :: ast.range_expr(&state.tree, loop.iterable)
start :: try type_snapshot(state, execution, bounds.start)
end :: try type_snapshot(state, execution, bounds.end)
if (start == no_node or end == no_node) return .invalid
typed_bound :: if (flexible(state, start)) end else start
annotation :: try expression_type(state, typed_bound, 0)
if (annotation == no_node or !try integer_type(state, annotation)) return .invalid
current := try type_execution_value(state, start, annotation)
limit :: try type_execution_value(state, end, annotation)
if (current == no_node or limit == no_node) return .invalid
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
if (loop.capture) |capture| try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = current, annotation = annotation,
})
execution.loop_depth += 1
defer execution.loop_depth -= 1
while (true) {
if (!try type_step(state, statement)) return .invalid
span :: ast.node_span(&state.tree, statement)
operator ast.BinaryOperator :: if (bounds.inclusive) .less_equal else .less
comparison :: try ast.add_binary(state.builder, span, operator, current, limit)
try refresh(state)
keep :: try evaluate(state, comparison)
if (keep.kind != .boolean) return .invalid
if (!keep.boolean) return .next
if (loop.capture != null) execution.locals.items[saved].value = current
flow :: try execute_type_block(state, execution, loop.body)
if (flow == .returned or flow == .yielded or flow == .invalid) return flow
if (flow == .break) return .next
if (try value_equal(state, try evaluate(state, current), try evaluate(state, limit))) return .next
one :: try ast.add_integer(state.builder, span, 1)
increment :: try ast.add_binary(state.builder, span, .add, current, one)
try refresh(state)
current = try type_execution_value(state, increment, annotation)
if (current == no_node) return .invalid
}
}
@hide:file
mix proc(left, right usize) usize {
return (left << 7) xor (left >> (sizeof!(usize) * 8 - 7)) xor right
}
@hide:file
evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
if (id == no_node) return Value{}
if (state.depth >= 128) return Value{}
state.depth += 1
defer state.depth -= 1
try expand(state, id)
kind :: ast.expr_kind(&state.tree, id)
resolved :: binding(state, id)
if (resolved.kind == .builtin and resolved.data.builtin != .type) return Value{ kind = .type, node = id }
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
class :: ast.node_class(&state.tree, target)
if (class == .function) {
if (template(state, target)) return Value{}
return Value{ kind = .function, node = id }
}
if (class == .statement) {
declaration :: ast.statement_decl(&state.tree, target)
if (declaration.mutability != .constant) return Value{}
value := try evaluate(state, declaration.initializer)
if (declaration.type) |annotation| value.annotation = annotation
return value
}
return Value{}
}
match kind {
.union_constant: {
value :: ast.union_constant(&state.tree, id)
return Value{ kind = .aggregate, node = id, annotation = value.type }
}
.typed_constant: {
view :: ast.typed_constant(&state.tree, id)
if (!is_type(state, view.type) and type_record_storage(state, view.type, 0)) {
if (!try type_array_initialized(state, id, id)) return Value{}
return Value{ kind = .aggregate, node = view.value, annotation = view.type }
}
value := try evaluate_expected(state, view.value, view.type)
value.annotation = view.type
return value
}
.literal_int: {
value := u64(0)
for ast.literal_text(&state.tree, id) |byte| {
if (byte < '0' or byte > '9') return Value{}
digit :: u64(byte - '0')
if (value > divtrunc!(maxval!(u64) - digit, u64(10))) return Value{}
value = value * 10 + digit
}
return Value{ kind = .integer, node = id, magnitude = value }
}
.literal_float: {
if (ast.float_value(&state.tree, id)) |value| return Value{
kind = .real, node = id, magnitude = ptrcast!(@u64, &value)^,
}
return try evaluate_ir(state, id, try builtin(state, id, .float))
}
.literal_bool: return Value{ kind = .boolean, node = id,
boolean = mem.eql(ast.literal_text(&state.tree, id), "true") }
.literal_string: {
text :: ast.literal_text(&state.tree, id)
cursor := usize(1)
while (cursor < text.len - 1) {
_ = ast.string_next(text, &cursor) catch {
try reject(state, id, .invalid_escape)
return Value{}
}
}
return Value{ kind = .string, node = id }
}
.literal_null: return Value{ kind = .null, node = id }
.literal_void: return Value{ kind = .aggregate, node = id, annotation = try builtin(state, id, .void) }
.record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type,
.enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type:
return Value{ kind = .type, node = id }
.alias_expr: return try evaluate(state, ast.type_operand(&state.tree, id))
.unary: {
view :: ast.unary_expr(&state.tree, id)
if (view.operator == .bit_not)
return try evaluate_ir(state, id, try expression_type(state, id, 0))
if (view.operator == .unwrap) {
# source-known callbacks already own declaration identities, not interpreter handles.
value := try evaluate(state, view.operand)
annotation :: try expression_type(state, view.operand, 0)
if (value.kind == .function and annotation != no_node
and ast.expr_kind(&state.tree, annotation) == .optional_type) {
try arraylist.append(&state.rewrites.constraints, id)
value.annotation = ast.type_operand(&state.tree, annotation)
return value
}
return try evaluate_ir(state, id, try expression_type(state, id, 0))
}
value := try evaluate(state, view.operand)
if (view.operator == .negate and value.kind == .integer) value.negative = value.magnitude != 0 and !value.negative
else if (view.operator == .negate and value.kind == .real) value.magnitude = value.magnitude xor (u64(1) << 63)
else if (view.operator == .logical_not and value.kind == .boolean) value.boolean = !value.boolean
else return Value{}
value.node = id
return value
}
.binary: return try evaluate_binary(state, id)
.call: return try evaluate_call(state, id)
.block_expr: {
annotation :: try expression_type(state, id, 0)
if (annotation == no_node) return Value{}
if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) {
result :: try evaluate_type_value_block(state, id, annotation)
if (result == no_node) return Value{}
return try evaluate_expected(state, result, annotation)
}
return try evaluate_ir(state, id, annotation)
}
.match_expr: {
annotation :: try expression_type(state, id, 0)
if (annotation != no_node and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
selected :: try type_value_expression(state, id)
if (selected == no_node or selected == id) return Value{}
return try evaluate_expected(state, selected, annotation)
}
return try evaluate_ir(state, id, annotation)
}
.record_literal, .array_literal, .selection, .index, .builtin_call, .catch_expr:
return try evaluate_ir(state, id, try expression_type(state, id, 0))
else: return Value{}
}
}
@hide:file
evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error {
view :: ast.binary_expr(&state.tree, id)
if (view.operator == .divide) return try evaluate_ir(state, id, try builtin(state, id, .float))
if (view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor
or view.operator == .shift_left or view.operator == .shift_right or view.operator == .shift_left_saturating)
return try evaluate_ir(state, id, try expression_type(state, id, 0))
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 }
}
right := try evaluate(state, view.right)
if (left.kind == .invalid or right.kind == .invalid) return Value{}
if (left.kind == .real or right.kind == .real) {
boolean :: (
view.operator == .equal or view.operator == .not_equal or view.operator == .less
or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal
)
kind sema.BuiltinType :: if (boolean) .bool else .float
result_type :: try builtin(state, id, kind)
return try evaluate_ir(state, id, result_type)
}
if (
left.annotation != no_node and right.annotation != no_node
and left.kind == .integer and right.kind == .integer
and !try same_type(state, left.annotation, right.annotation)
) return Value{}
if (view.operator == .equal or view.operator == .not_equal) {
if (left.kind != right.kind) return Value{}
equal :: try value_equal(state, left, right)
if (left.kind == .type) {
operands [2]ast.NodeId :: [view.left, view.right]
for (operands) |operand| {
if (type_record_storage(state, operand, 0)) continue
constraint :: try ast.add_type_operand(state.builder,
ast.node_span(&state.tree, operand), operand, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
}
folded :: try ast.add_boolean(state.builder, ast.node_span(&state.tree, id),
equal == (view.operator == .equal))
try replace(state, id, folded)
try refresh(state)
}
return Value{ kind = .boolean, node = id, boolean = equal == (view.operator == .equal) }
}
if (left.kind == .boolean and right.kind == .boolean) {
if (view.operator == .logical_and) return Value{ kind = .boolean, node = id, boolean = left.boolean and right.boolean }
if (view.operator == .logical_or) return Value{ kind = .boolean, node = id, boolean = left.boolean or right.boolean }
return Value{}
}
if (left.kind != .integer or right.kind != .integer) return Value{}
annotation :: if (left.annotation != no_node) left.annotation else right.annotation
less :: if (left.negative != right.negative) left.negative
else if (left.negative) left.magnitude > right.magnitude else left.magnitude < right.magnitude
equal :: left.magnitude == right.magnitude and left.negative == right.negative
match view.operator {
.less: return Value{ kind = .boolean, node = id, boolean = less }
.less_equal: return Value{ kind = .boolean, node = id, boolean = less or equal }
.greater: return Value{ kind = .boolean, node = id, boolean = !less and !equal }
.greater_equal: return Value{ kind = .boolean, node = id, boolean = !less }
.multiply: {
if (right.magnitude != 0 and left.magnitude > divtrunc!(maxval!(u64), right.magnitude)) return Value{}
magnitude :: left.magnitude * right.magnitude
return try integer_value(state, id, magnitude, magnitude != 0 and left.negative != right.negative, annotation)
}
.add, .subtract: {
if (view.operator == .subtract and right.magnitude != 0) right.negative = !right.negative
if (left.negative == right.negative) {
if (left.magnitude > maxval!(u64) - right.magnitude) return Value{}
return try integer_value(state, id, left.magnitude + right.magnitude, left.negative, annotation)
}
if (left.magnitude < right.magnitude) return try integer_value(state, id, right.magnitude - left.magnitude, right.negative, annotation)
return try integer_value(state, id, left.magnitude - right.magnitude,
left.magnitude != right.magnitude and left.negative, annotation)
}
else: return Value{}
}
}
@hide:file
canonical_type proc(state @mut State, id ast.NodeId) ast.NodeId ! Error {
value :: try evaluate(state, id)
return if (value.kind == .type) value.node else no_node
}
# metadata storage compares anonymous annotation shapes, including ordinary nested
# fields. named definitions and nominal type results retain their original identity.
@hide:file
# ponytail: scan declaration syntax for nominal identity; index record origins if metadata-heavy builds make this hot.
structural_type_record proc(state @State, id ast.NodeId) bool {
for (state.instances.items) |instance| {
if (instance.type == id) return false
}
for (0..ast.node_count(&state.tree)) |i| {
declaration :: ast.NodeId(i)
if (ast.node_class(&state.tree, declaration) != .statement
or ast.stmt_kind(&state.tree, declaration) != .declaration) continue
if (ast.statement_decl(&state.tree, declaration).initializer == id) return false
}
return true
}
@hide:file
same_type proc(state @mut State, left, right ast.NodeId) bool ! Error {
return try same_type_context(state, left, right, false)
}
@hide:file
same_type_context proc(state @mut State, left, right ast.NodeId, metadata bool) bool ! Error {
if (is_type(state, left) or is_type(state, right))
return is_type(state, left) and is_type(state, right)
a :: try canonical_type(state, left)
b :: try canonical_type(state, right)
if (a == no_node or b == no_node) return false
if (a == b) return true
x :: binding(state, a)
y :: binding(state, b)
if (x.kind == .builtin or y.kind == .builtin) return x.kind == .builtin and y.kind == .builtin and builtin_identity(x.data.builtin) == builtin_identity(y.data.builtin)
kind :: ast.expr_kind(&state.tree, a)
if (kind == .sum_type or ast.expr_kind(&state.tree, b) == .sum_type)
return try type_subset(state, a, b) and try type_subset(state, b, a)
if (kind != ast.expr_kind(&state.tree, b)) return false
match kind {
.union_tag_type: return try same_type_context(state,
ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b), metadata)
.record_type: {
if (!metadata and (!type_record_storage(state, a, 0) or !type_record_storage(state, b, 0))) return false
if (!structural_type_record(state, a) or !structural_type_record(state, b)) return false
count :: ast.record_field_count(&state.tree, a)
if (count != ast.record_field_count(&state.tree, b)) return false
for (0..count) |i| {
p :: ast.field(&state.tree, ast.record_field(&state.tree, a, i))
q :: ast.field(&state.tree, ast.record_field(&state.tree, b, i))
if ((p.name == null) != (q.name == null)) return false
if (p.name != null and ast.name_id(&state.tree, p.name?) != ast.name_id(&state.tree, q.name?)) return false
if (p.type == null or q.type == null or !try same_type_context(state, p.type?, q.type?, true)) return false
}
return true
}
.pointer_type: {
p :: ast.pointer_type(&state.tree, a)
q :: ast.pointer_type(&state.tree, b)
return p.mutable == q.mutable and p.many == q.many and try same_type_context(state, p.element, q.element, metadata)
}
.array_type: {
p :: ast.array_type(&state.tree, a)
q :: ast.array_type(&state.tree, b)
if (p.mutable != q.mutable or p.count == null or q.count == null) return false
pv :: try evaluate(state, p.count?)
qv :: try evaluate(state, q.count?)
return try value_equal(state, pv, qv) and try same_type_context(state, p.element, q.element, metadata)
}
.slice_type: {
p :: ast.slice_type(&state.tree, a)
q :: ast.slice_type(&state.tree, b)
if (p.mutable != q.mutable or (p.sentinel == null) != (q.sentinel == null)) return false
if (p.sentinel) |sentinel| {
if (!try value_equal(state, try evaluate(state, sentinel), try evaluate(state, q.sentinel?))) return false
}
return try same_type_context(state, p.element, q.element, metadata)
}
.optional_type: return try same_type_context(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b), metadata)
.fallible_type: {
p :: ast.binary_expr(&state.tree, a)
q :: ast.binary_expr(&state.tree, b)
return try same_type_context(state, p.left, q.left, metadata) and try same_type_context(state, p.right, q.right, metadata)
}
.function_type: {
count :: ast.function_param_count(&state.tree, a)
if (count != ast.function_param_count(&state.tree, b)) return false
if (!try same_type_context(state, ast.callable_result(&state.tree, a), ast.callable_result(&state.tree, b), metadata)) return false
for (0..count) |i| {
p :: ast.param(&state.tree, ast.function_param(&state.tree, a, i)).type
q :: ast.param(&state.tree, ast.function_param(&state.tree, b, i)).type
if (!try same_type_context(state, p, q, metadata)) return false
}
return true
}
else: return false
}
}
@hide:file
type_subset proc(state @mut State, left, right ast.NodeId) bool ! Error {
a :: try canonical_type(state, left)
b :: try canonical_type(state, right)
if (a == no_node or b == no_node) return false
if (ast.expr_kind(&state.tree, a) == .sum_type) {
view :: ast.binary_expr(&state.tree, a)
return try type_subset(state, view.left, b) and try type_subset(state, view.right, b)
}
if (ast.expr_kind(&state.tree, b) == .sum_type) {
view :: ast.binary_expr(&state.tree, b)
return try type_subset(state, a, view.left) or try type_subset(state, a, view.right)
}
return try same_type(state, a, b)
}
@hide:file
value_equal proc(state @mut State, left, right Value) bool ! Error {
if (left.kind != right.kind) return false
return match left.kind {
.invalid, .null: true
.type: try same_type(state, left.node, right.node)
.integer: left.magnitude == right.magnitude and left.negative == right.negative
.boolean: left.boolean == right.boolean
.function: binding(state, left.node).data.declaration == binding(state, right.node).data.declaration
.real: left.magnitude == right.magnitude
.aggregate: (
try same_type(state, left.annotation, right.annotation)
and try constant_equal(state, left.node, right.node)
)
.string: try same_string(state, left.node, right.node)
}
}
@hide:file
value_hash proc(state @mut State, value Value) usize ! Error {
hash :: enum_index(value.kind)
negative :: if (value.negative) usize(1) else usize(0)
boolean :: if (value.boolean) usize(1) else usize(0)
return match value.kind {
.type: mix(hash, try type_hash(state, value.node))
.integer: mix(mix(hash, usize(value.magnitude)), negative)
.boolean: mix(hash, boolean)
.function: mix(hash, usize(binding(state, value.node).data.declaration))
.string: string_hash(hash, ast.literal_text(&state.tree, value.node))
.real: mix(hash, usize(value.magnitude))
.aggregate: mix(mix(hash, try type_hash(state, value.annotation)), try constant_hash(state, value.node))
.invalid, .null: hash
}
}
@hide:file
string_hash proc(seed usize, text []u8) usize {
result := seed
cursor := usize(1)
while (cursor < text.len - 1) {
byte :: ast.string_next(text, &cursor) catch { return 0 }
result = mix(result, usize(byte))
}
return result
}
@hide:file
type_hash proc(state @mut State, id ast.NodeId) usize ! Error {
return try type_hash_context(state, id, false)
}
@hide:file
type_hash_context proc(state @mut State, id ast.NodeId, metadata bool) usize ! Error {
node :: try canonical_type(state, id)
if (node == no_node) return 0
resolved :: binding(state, node)
if (resolved.kind == .builtin) return mix(1, enum_index(builtin_identity(resolved.data.builtin)))
kind :: ast.expr_kind(&state.tree, node)
hash := enum_index(kind)
match kind {
.union_tag_type: return mix(hash,
try type_hash_context(state, ast.type_operand(&state.tree, node), metadata))
.record_type: {
if ((!metadata and !type_record_storage(state, node, 0))
or !structural_type_record(state, node)) return mix(hash, usize(node))
for (0..ast.record_field_count(&state.tree, node)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, node, i))
if (field.name) |name| hash = mix(hash, usize(ast.name_id(&state.tree, name)))
if (field.type) |annotation| hash = mix(hash, try type_hash_context(state, annotation, true))
}
return hash
}
.pointer_type: {
view :: ast.pointer_type(&state.tree, node)
mutable :: if (view.mutable) usize(1) else usize(0)
many :: if (view.many) usize(1) else usize(0)
hash = mix(hash, mutable)
hash = mix(hash, many)
return mix(hash, try type_hash_context(state, view.element, metadata))
}
.array_type: {
view :: ast.array_type(&state.tree, node)
mutable :: if (view.mutable) usize(1) else usize(0)
hash = mix(hash, mutable)
if (view.count) |count| hash = mix(hash, try value_hash(state, try evaluate(state, count)))
return mix(hash, try type_hash_context(state, view.element, metadata))
}
.slice_type: {
view :: ast.slice_type(&state.tree, node)
mutable :: if (view.mutable) usize(1) else usize(0)
hash = mix(hash, mutable)
if (view.sentinel) |sentinel| hash = mix(hash, try value_hash(state, try evaluate(state, sentinel)))
return mix(hash, try type_hash_context(state, view.element, metadata))
}
.optional_type: return mix(hash, try type_hash_context(state, ast.type_operand(&state.tree, node), metadata))
.sum_type: {
view :: ast.binary_expr(&state.tree, node)
# set fingerprints must ignore ordering, grouping and repeated members.
# colliding fingerprints still use full type equality in the cache chain.
return try type_hash_context(state, view.left, metadata) | try type_hash_context(state, view.right, metadata)
}
.fallible_type: {
view :: ast.binary_expr(&state.tree, node)
return mix(mix(hash, try type_hash_context(state, view.left, metadata)), try type_hash_context(state, view.right, metadata))
}
.function_type: {
hash = mix(hash, try type_hash_context(state, ast.callable_result(&state.tree, node), metadata))
for (0..ast.function_param_count(&state.tree, node)) |i| {
parameter :: ast.function_param(&state.tree, node, i)
hash = mix(hash, try type_hash_context(state, ast.param(&state.tree, parameter).type, metadata))
}
return hash
}
else: return mix(hash, usize(node))
}
}
@hide:file
value_block_yield proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error {
if (depth >= 128) return no_node
class :: ast.node_class(&state.tree, id)
if (class != .block and class != .statement) return no_node
if (class == .statement) {
kind :: ast.stmt_kind(&state.tree, id)
if (kind == .yield_stmt)
return ast.yield_stmt(&state.tree, id).value?
if (kind != .block_stmt and kind != .if_stmt and kind != .while_stmt and kind != .for_stmt)
return no_node
}
result := no_node
for (0..ast.child_count(&state.tree, id)) |i| {
child :: ast.child(&state.tree, id, i)
value :: try value_block_yield(state, child, depth + 1)
if (value == no_node or try expression_type(state, value, depth + 1) == no_node) continue
result = value
if (!flexible(state, value)) return result
}
return result
}
@hide:file
value_block_type proc(state @mut State, block ast.NodeId, depth usize) ast.NodeId ! Error {
value :: try value_block_yield(state, block, depth)
return if (value != no_node) try expression_type(state, value, depth + 1)
else try builtin(state, block, .void)
}
@hide:file
expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error {
if (id == no_node) return no_node
if (depth >= 128) return no_node
try expand(state, id)
resolved :: binding(state, id)
if (resolved.kind == .builtin and resolved.data.builtin != .type) return try builtin(state, id, .type)
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
if (hashmap.get(&state.capture_types, target)) |annotation| return try canonical_type(state, annotation)
class :: ast.node_class(&state.tree, target)
if (class == .parameter) {
annotation :: ast.param(&state.tree, target).type
return try canonical_type(state, annotation)
}
if (class == .statement) {
declaration :: ast.statement_decl(&state.tree, target)
if (declaration.type) |annotation| return try canonical_type(state, annotation)
return try expression_type(state, declaration.initializer, depth + 1)
}
if (class == .function) {
params := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&params)
for (0..ast.function_param_count(&state.tree, target)) |i|
try arraylist.append(&params, ast.function_param(&state.tree, target, i))
result :: try ast.add_function_type(state.builder, ast.node_span(&state.tree, id),
ast.function_decl(&state.tree, target).return_type, params.items)
try refresh(state)
return result
}
return no_node
}
kind :: ast.expr_kind(&state.tree, id)
match kind {
.record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type,
.enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr:
return try builtin(state, id, .type)
.typed_constant, .union_constant: {
annotation :: if (kind == .union_constant) ast.union_constant(&state.tree, id).type
else ast.typed_constant(&state.tree, id).type
return if (is_type(state, annotation)) annotation else try canonical_type(state, annotation)
}
.literal_int: return try builtin(state, id, .i64)
.literal_float: return try builtin(state, id, .f64)
.literal_bool: return try builtin(state, id, .bool)
.literal_string: return try builtin(state, id, .string)
.literal_void: return try builtin(state, id, .void)
.block_expr: return try value_block_type(state, ast.block_expr(&state.tree, id), depth + 1)
.match_expr: {
result := no_node
for (0..ast.match_arm_count(&state.tree, id)) |i| {
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, i))
annotation :: if (arm.expression) try expression_type(state, arm.body, depth + 1)
else try value_block_type(state, arm.body, depth + 1)
if (annotation == no_node) continue
result = annotation
if (!arm.expression or !flexible(state, arm.body)) return result
}
return result
}
.builtin_call: {
name :: ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name)
count :: ast.call_arg_count(&state.tree, id)
if ((mem.eql(name, "sizeof") or mem.eql(name, "alignof")) and count == 1)
return try builtin(state, id, .usize)
if ((mem.eql(name, "maxval") or mem.eql(name, "minval")) and count == 1)
return try canonical_type(state, ast.call_arg(&state.tree, id, 0))
if ((mem.eql(name, "ptrcast") or mem.eql(name, "constcast")) and count == 2)
return try canonical_type(state, ast.call_arg(&state.tree, id, 0))
if (mem.eql(name, "divtrunc") and count == 2) {
left :: ast.call_arg(&state.tree, id, 0)
right :: ast.call_arg(&state.tree, id, 1)
operand :: if (flexible(state, left)) right else left
return try expression_type(state, operand, depth + 1)
}
return no_node
}
.catch_expr, .try_expr: {
operand :: if (kind == .catch_expr) ast.catch_expr(&state.tree, id).operand
else ast.type_operand(&state.tree, id)
type :: try expression_type(state, operand, depth + 1)
if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node
return try canonical_type(state, ast.binary_expr(&state.tree, type).left)
}
.record_literal: {
view :: ast.record_literal(&state.tree, id)
if (view.type) |annotation| return try canonical_type(state, annotation)
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
if (field.value == null) return no_node
type :: try expression_type(state, field.value?, depth + 1)
if (type == no_node) return no_node
generated :: try ast.add_field(state.builder, field.span, field.name, type, null)
try refresh(state)
try arraylist.append(&fields, generated)
}
result :: try ast.add_record_type(state.builder, ast.node_span(&state.tree, id), fields.items)
try refresh(state)
return result
}
.array_literal: {
count :: ast.array_element_count(&state.tree, id)
if (count == 0) return no_node
element := try expression_type(state, ast.array_element(&state.tree, id, 0), depth + 1)
if (element == no_node) return no_node
# inferred values have concrete default widths even when a cast or
# procedure result used the int/float annotation shorthand.
scalar :: binding(state, element)
if (scalar.kind == .builtin) {
if (scalar.data.builtin == .int) element = try builtin(state, element, .i64)
if (scalar.data.builtin == .float) element = try builtin(state, element, .f64)
}
length :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id), u64(count))
try refresh(state)
result :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, id), length, element, false)
try refresh(state)
return result
}
.call: {
callee :: ast.call_expr(&state.tree, id).callee
destination :: binding(state, callee)
if (destination.kind == .builtin) return callee
if (destination.kind == .declaration and ast.node_class(&state.tree, destination.data.declaration) == .function) return try canonical_type(state, ast.function_decl(&state.tree, destination.data.declaration).return_type)
signature :: try expression_type(state, callee, depth + 1)
if (signature != no_node and is_type(state, signature)) return try canonical_type(state, callee)
if (signature != no_node and ast.expr_kind(&state.tree, signature) == .function_type)
return try canonical_type(state, ast.callable_result(&state.tree, signature))
return no_node
}
.unary: {
view :: ast.unary_expr(&state.tree, id)
operand :: try expression_type(state, view.operand, depth + 1)
if (operand == no_node) return no_node
if (view.operator == .address_of) {
mutable := false
target :: binding(state, view.operand)
if (target.kind == .declaration and ast.node_class(&state.tree, target.data.declaration) == .statement)
mutable = ast.statement_decl(&state.tree, target.data.declaration).mutability == .variable
result :: try ast.add_pointer_type(state.builder, view.span, operand, mutable, false)
try refresh(state)
return result
}
if (view.operator == .dereference and ast.expr_kind(&state.tree, operand) == .pointer_type)
return try canonical_type(state, ast.pointer_type(&state.tree, operand).element)
if (view.operator == .unwrap and ast.expr_kind(&state.tree, operand) == .optional_type)
return try canonical_type(state, ast.type_operand(&state.tree, operand))
return operand
}
.binary: {
view :: ast.binary_expr(&state.tree, id)
if (
view.operator == .equal or view.operator == .not_equal or view.operator == .less
or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal
or view.operator == .logical_and or view.operator == .logical_or
) return try builtin(state, id, .bool)
if ((view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor)
and flexible(state, view.left)) return try expression_type(state, view.right, depth + 1)
return try expression_type(state, view.left, depth + 1)
}
.selection: {
view :: ast.selection_expr(&state.tree, id)
base := try expression_type(state, view.base, depth + 1)
if (base == no_node) return no_node
if (is_type(state, base)) {
selected :: try canonical_type(state, view.base)
if (selected != no_node and ast.expr_kind(&state.tree, selected) == .enum_type) return selected
}
if (ast.expr_kind(&state.tree, base) == .pointer_type) base = try canonical_type(state, ast.pointer_type(&state.tree, base).element)
if (base == no_node) return no_node
member :: ast.literal_text(&state.tree, view.member)
base_kind :: ast.expr_kind(&state.tree, base)
if (
mem.eql(member, "len") and (
base_kind == .array_type or base_kind == .slice_type
or (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string)
)
)
return try builtin(state, id, .usize)
if (mem.eql(member, "ptr")) {
element := no_node
mutable := false
if (base_kind == .array_type) {
element = ast.array_type(&state.tree, base).element
mutable = ast.array_type(&state.tree, base).mutable
}
if (base_kind == .slice_type) {
element = ast.slice_type(&state.tree, base).element
mutable = ast.slice_type(&state.tree, base).mutable
}
if (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string)
element = try builtin(state, id, .u8)
if (element != no_node) {
result :: try ast.add_pointer_type(state.builder, view.span, element, mutable, true)
try refresh(state)
return result
}
}
if (base_kind != .record_type and base_kind != .union_type) return no_node
slot :: type_record_slot(state, base, view.member)
if (slot == no_index) return no_node
return try canonical_type(state, ast.field(&state.tree, ast.record_field(&state.tree, base, slot)).type?)
}
.index: {
view :: ast.index_expr(&state.tree, id)
base :: try expression_type(state, view.base, depth + 1)
if (base == no_node) return no_node
base_kind :: ast.expr_kind(&state.tree, base)
if (base_kind == .array_type) {
element :: ast.array_type(&state.tree, base).element
return if (is_type(state, element)) element else try canonical_type(state, element)
}
if (base_kind == .slice_type) return try canonical_type(state, ast.slice_type(&state.tree, base).element)
if (base_kind == .pointer_type) return try canonical_type(state, ast.pointer_type(&state.tree, base).element)
if (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string) return try builtin(state, id, .u8)
return no_node
}
.slice: {
view :: ast.slice_expr(&state.tree, id)
base :: try expression_type(state, view.base, depth + 1)
if (base == no_node) return no_node
base_kind :: ast.expr_kind(&state.tree, base)
element := no_node
mutable := false
if (base_kind == .array_type) {
element = ast.array_type(&state.tree, base).element
mutable = ast.array_type(&state.tree, base).mutable
}
if (base_kind == .pointer_type) {
element = ast.pointer_type(&state.tree, base).element
mutable = ast.pointer_type(&state.tree, base).mutable
}
if (base_kind == .slice_type) {
element = ast.slice_type(&state.tree, base).element
mutable = ast.slice_type(&state.tree, base).mutable
}
if (element == no_node) return no_node
result :: try ast.add_slice_type(state.builder, ast.node_span(&state.tree, id), element, mutable, null)
try refresh(state)
return result
}
else: return no_node
}
}
@hide:file
parameter_index proc(state @State, function, id ast.NodeId) usize {
for (0..ast.function_param_count(&state.tree, function)) |i| {
if (ast.function_param(&state.tree, function, i) == id and ast.param(&state.tree, id).comptime) return i
}
return no_index
}
@hide:file
infer_value proc(state @mut State, function, pattern ast.NodeId, actual Value, arguments []mut Value, contextual bool) bool ! Error {
resolved :: binding(state, pattern)
if (resolved.kind == .declaration) {
index :: parameter_index(state, function, resolved.data.declaration)
if (index != no_index) {
if (arguments[index].kind == .invalid) {
arguments[index] = actual
return true
}
if (contextual and arguments[index].kind == .type and actual.kind == .type) return true
return try value_equal(state, arguments[index], actual)
}
}
return try value_equal(state, try evaluate(state, pattern), actual)
}
@hide:file
infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []mut Value, contextual bool) bool ! Error {
resolved :: binding(state, pattern)
if (resolved.kind == .declaration) {
index :: parameter_index(state, function, resolved.data.declaration)
if (index != no_index) {
if (arguments[index].kind == .type) {
expected :: try canonical_type(state, arguments[index].node)
# a fixed slice type supplies borrowing/string context; ordinary
# argument checking owns element, mutability and sentinel validation.
if (expected != no_node and ast.expr_kind(&state.tree, expected) == .slice_type) return true
}
return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual)
}
}
# only normalize the actual type: the pattern may still contain unbound
# parameters or a type-generator call whose arguments must be inferred.
concrete :: try canonical_type(state, actual)
if (concrete == no_node) return false
kind :: ast.expr_kind(&state.tree, pattern)
actual_kind :: ast.expr_kind(&state.tree, concrete)
match kind {
.pointer_type: {
if (actual_kind != .pointer_type) return false
return try infer(state, function, ast.pointer_type(&state.tree, pattern).element,
ast.pointer_type(&state.tree, concrete).element, arguments, contextual)
}
.slice_type: {
element := no_node
if (actual_kind == .slice_type) element = ast.slice_type(&state.tree, concrete).element
if (actual_kind == .array_type) element = ast.array_type(&state.tree, concrete).element
if (element == no_node) return false
return try infer(state, function, ast.slice_type(&state.tree, pattern).element, element, arguments, contextual)
}
.array_type: {
if (actual_kind != .array_type) return false
p :: ast.array_type(&state.tree, pattern)
a :: ast.array_type(&state.tree, concrete)
if (p.count) |count| {
if (a.count == null or !try infer_value(state, function, count, try evaluate(state, a.count?), arguments, false)) return false
}
return try infer(state, function, p.element, a.element, arguments, contextual)
}
.optional_type: {
element :: if (actual_kind == .optional_type) ast.type_operand(&state.tree, concrete) else concrete
return try infer(state, function, ast.type_operand(&state.tree, pattern), element, arguments, contextual)
}
.fallible_type: {
if (actual_kind != .fallible_type) return false
p :: ast.binary_expr(&state.tree, pattern)
a :: ast.binary_expr(&state.tree, concrete)
return (try infer(state, function, p.left, a.left, arguments, contextual)
and try infer(state, function, p.right, a.right, arguments, contextual))
}
.function_type: {
if (actual_kind != .function_type) return false
count :: ast.function_param_count(&state.tree, pattern)
if (count != ast.function_param_count(&state.tree, concrete)) return false
for (0..count) |i| {
p :: ast.param(&state.tree, ast.function_param(&state.tree, pattern, i)).type
a :: ast.param(&state.tree, ast.function_param(&state.tree, concrete, i)).type
if (!try infer(state, function, p, a, arguments, contextual)) return false
}
return try infer(state, function, ast.callable_result(&state.tree, pattern),
ast.callable_result(&state.tree, concrete), arguments, contextual)
}
.call: {
callee :: binding(state, ast.call_expr(&state.tree, pattern).callee)
if (callee.kind != .declaration) return false
for (0..(state.instances.items.len)) |i| {
instance :: state.instances.items[i]
if (instance.type == no_node or instance.function != callee.data.declaration) continue
if (try canonical_type(state, instance.type) != concrete) continue
count :: ast.call_arg_count(&state.tree, pattern)
if (count != instance.arguments.len) return false
for (0..count) |j| {
if (!try infer_value(state, function, ast.call_arg(&state.tree, pattern, j), instance.arguments[j], arguments, contextual)) return false
}
return true
}
return false
}
else: return true
}
}
@hide:file
infer_result proc(
state @mut State,
function, pattern, actual ast.NodeId,
arguments []mut Value,
) bool ! Error {
concrete :: try canonical_type(state, actual)
if (concrete == no_node) return true
if (
ast.expr_kind(&state.tree, concrete) == .optional_type
and ast.expr_kind(&state.tree, pattern) != .optional_type
) {
resolved :: binding(state, pattern)
if (resolved.kind == .declaration) {
index :: parameter_index(state, function, resolved.data.declaration)
if (index != no_index) {
if (arguments[index].kind == .invalid)
return try infer(state, function, pattern, concrete, arguments, false)
if (arguments[index].kind == .type and try same_type(state, arguments[index].node, concrete)) return true
}
}
return try infer_result(state, function, pattern, ast.type_operand(&state.tree, concrete), arguments)
}
if (
ast.expr_kind(&state.tree, concrete) == .fallible_type
and ast.expr_kind(&state.tree, pattern) != .fallible_type
) return try infer_result(state, function, pattern, ast.binary_expr(&state.tree, concrete).left, arguments)
if (ast.expr_kind(&state.tree, pattern) == .fallible_type) {
view :: ast.binary_expr(&state.tree, pattern)
if (ast.expr_kind(&state.tree, concrete) != .fallible_type)
return try infer_result(state, function, view.left, concrete, arguments)
channels :: ast.binary_expr(&state.tree, concrete)
return (try infer_result(state, function, view.left, channels.left, arguments)
and try infer_result(state, function, view.right, channels.right, arguments))
}
return try infer(state, function, pattern, concrete, arguments, false)
}
@hide:file
materialize proc(state @mut State, value Value) Value ! Error {
result := value
span :: ast.node_span(&state.tree, value.node)
if (value.kind == .type and nominal_type_value(state, value.node)) {
# refer to declared nominal types, rather than reusing their definition as
# an annotation and turning its value-role checker cell into an annotation.
for (state.declarations.items) |declaration| {
if (
ast.node_class(&state.tree, declaration) == .statement
and ast.statement_decl(&state.tree, declaration).initializer == value.node
) {
result.node = try reference(state, value.node, declaration)
return result
}
}
}
if (value.kind == .integer) {
result.node = try ast.add_integer(state.builder, span, value.magnitude)
if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node)
} else if (value.kind == .boolean) {
result.node = try ast.add_boolean(state.builder, span, value.boolean)
} else if (value.kind == .real) {
result.node = try ast.add_float(state.builder, span, ptrcast!(@f64, &value.magnitude)^)
}
try refresh(state)
return result
}
@hide:file
valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Error {
type :: try canonical_type(state, annotation)
if (type == no_node) return false
resolved :: binding(state, type)
if (resolved.kind == .builtin) {
kind :: resolved.data.builtin
if (kind == .bool) return value.kind == .boolean
if (kind == .string) return value.kind == .string
if (kind == .float or kind == .f64) return value.kind == .real
if (kind == .void) return (
value.kind == .aggregate and value.annotation != no_node
and try same_type(state, type, value.annotation)
)
if (value.kind != .integer) return false
bits := u32(64)
signed := false
match kind {
.i8, .c_char: {
bits = 8
signed = true
}
.i16, .c_short: {
bits = 16
signed = true
}
.i32, .c_int: {
bits = 32
signed = true
}
.int, .i64, .isize, .c_long, .c_longlong: signed = true
.u8, .c_uchar: bits = 8
.u16, .c_ushort: bits = 16
.u32, .c_uint: bits = 32
.u64, .uint, .usize, .c_ulong, .c_ulonglong: {}
else: return false
}
limit := maxval!(u64)
if (signed) {
limit = u64(1) << (bits - 1)
if (!value.negative) limit -= 1
} else {
if (value.negative) return false
if (bits != 64) limit = (u64(1) << bits) - 1
}
return value.magnitude <= limit
}
kind :: ast.expr_kind(&state.tree, type)
if (value.kind == .aggregate) return value.annotation != no_node and try same_type(state, type, value.annotation)
if (kind == .optional_type) return value.kind == .null or try valid_value(state, ast.type_operand(&state.tree, type), value)
if (kind == .slice_type and value.kind == .string) {
view :: ast.slice_type(&state.tree, type)
child :: try canonical_type(state, view.element)
return (
!view.mutable and child != no_node and binding(state, child).kind == .builtin
and binding(state, child).data.builtin == .u8
)
}
if (kind == .function_type and value.kind == .function) {
actual :: try expression_type(state, value.node, 0)
return actual != no_node and try same_type(state, type, actual)
}
if (kind == .distinct_type) return try valid_value(state, ast.type_operand(&state.tree, type), value)
return false
}
@hide:file
enum_index proc($T type, value T) usize {
match typeinfo!(T) {
.enum |info|: {
inline for (info.fields) |field| {
if (value == field!(T, field.name)) return field.index
}
unreachable
}
else: unreachable
}
}
@hide:file
remember proc(state @mut State, target ast.NodeId) void ! mem.AllocError {
if (usize(target) >= state.original_count) return
saved :: try ast.copy_node(state.builder, target)
try arraylist.append(&state.rewrites.changes, Rewrite{ target = target, saved = saved })
}
@hide:file
replace proc(state @mut State, target, source ast.NodeId) void ! mem.AllocError {
try remember(state, target)
ast.replace_node(state.builder, target, source)
}
@hide:file
builtin_identities :: $enummap.init_default(
sema.BuiltinType,
sema.BuiltinType,
sema.BuiltinType.type, {
i64 = .int, c_long = .int, c_longlong = .int,
f64 = .float,
uint = .u64, c_ulong = .u64, c_ulonglong = .u64,
c_char = .i8,
c_uchar = .u8,
c_short = .i16,
c_ushort = .u16,
c_int = .i32,
c_uint = .u32,
})
@hide:file
builtin_identity proc(kind sema.BuiltinType) sema.BuiltinType {
if (enummap.get(&builtin_identities, kind)) |identity| return identity
return kind
}
@hide:file
integer_type proc(state @mut State, annotation ast.NodeId) bool ! Error {
type :: try canonical_type(state, annotation)
if (type == no_node) return false
resolved :: binding(state, type)
if (resolved.kind == .builtin) {
return match builtin_identity(resolved.data.builtin) {
.int, .i8, .i16, .i32, .isize, .u8, .u16, .u32, .u64, .usize: true
else: false
}
}
if (ast.expr_kind(&state.tree, type) == .distinct_type) return try integer_type(state, ast.type_operand(&state.tree, type))
return false
}
@hide:file
integer_value proc(state @mut State, id ast.NodeId, magnitude u64, negative bool, annotation ast.NodeId) Value ! Error {
result :: Value{ kind = .integer, node = id, magnitude = magnitude, negative = negative, annotation = annotation }
if (annotation != no_node and !try valid_value(state, annotation, result)) return Value{}
return result
}
@hide:file
same_string proc(state @State, left, right ast.NodeId) bool ! Error {
a :: ast.literal_text(&state.tree, left)
b :: ast.literal_text(&state.tree, right)
ac := usize(1)
bc := usize(1)
while (ac < a.len - 1 and bc < b.len - 1) {
av :: ast.string_next(a, &ac) catch { return false }
bv :: ast.string_next(b, &bc) catch { return false }
if (av != bv) return false
}
return ac == a.len - 1 and bc == b.len - 1
}
@hide:file
flexible proc(state @State, id ast.NodeId) bool {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .literal_int or kind == .literal_float) return true
if (kind == .unary) {
view :: ast.unary_expr(&state.tree, id)
return (view.operator == .negate or view.operator == .bit_not) and flexible(state, view.operand)
}
if (kind == .binary) {
view :: ast.binary_expr(&state.tree, id)
return (
(view.operator == .add or view.operator == .subtract or view.operator == .multiply or view.operator == .divide
or view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor
or view.operator == .shift_left or view.operator == .shift_right or view.operator == .shift_left_saturating)
and flexible(state, view.left) and flexible(state, view.right)
)
}
return false
}
@hide:file
template proc(state @State, id ast.NodeId) bool {
return ast.function_generic(&state.tree, id) or compiletime_result(state, ast.function_decl(&state.tree, id).return_type)
}