Files
honey/source/sema/resolve.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

704 lines
28 KiB
Plaintext

import "@std/debug"
import "@std/mem"
import "@std/hashmap"
import "@std/arraylist"
import "@source/strpool"
import "@source/ast"
import "@source/diagnostics"
BuiltinType :: enum {
int, float,
bool, string,
void, i64,
f64, usize,
i8, i16, i32, isize,
u8, u16, u32, u64, uint,
anyopaque
c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong
c_char, c_uchar, c_short, c_ushort
type
unavailable
}
@hide:file
Builtin :: struct {
name []u8
type BuiltinType
}
@hide:file
builtins [_]Builtin :: [
{ name = "int", type = .int },
{ name = "float", type = .float },
{ name = "bool", type = .bool },
{ name = "string", type = .string },
{ name = "void", type = .void },
{ name = "i64", type = .i64 },
{ name = "f64", type = .f64 },
{ name = "usize", type = .usize },
{ name = "i8", type = .i8 },
{ name = "i16", type = .i16 },
{ name = "i32", type = .i32 },
{ name = "isize", type = .isize },
{ name = "u8", type = .u8 },
{ name = "u16", type = .u16 },
{ name = "u32", type = .u32 },
{ name = "u64", type = .u64 },
{ name = "uint", type = .uint },
{ name = "anyopaque", type = .anyopaque },
{ name = "c_int", type = .c_int },
{ name = "c_uint", type = .c_uint },
{ name = "c_long", type = .c_long },
{ name = "c_ulong", type = .c_ulong },
{ name = "c_longlong", type = .c_longlong },
{ name = "c_ulonglong", type = .c_ulonglong },
{ name = "c_char", type = .c_char },
{ name = "c_schar", type = .c_char },
{ name = "c_uchar", type = .c_uchar },
{ name = "c_short", type = .c_short },
{ name = "c_ushort", type = .c_ushort },
{ name = "c_double", type = .f64 },
{ name = "c_float", type = .unavailable },
{ name = "c_longdouble", type = .unavailable },
{ name = "type", type = .type },
]
@hide:file
intern_builtins proc(
$N usize,
pool @mut strpool.StringPool,
entries @[N]Builtin,
) [N]mut strpool.StringId ! (mem.AllocError | strpool.InternError) {
ids [N]mut strpool.StringId := undefined
for (entries) |builtin, index| ids[index] = try strpool.intern(pool, builtin.name)
return ids
}
BindingKind :: enum { none, declaration, namespace, builtin, invalid }
BindingData :: union {
declaration ast.NodeId
namespace ast.NodeId
builtin BuiltinType
diagnostic diagnostics.DiagnosticId
}
Binding :: struct {
kind BindingKind = .none
data BindingData = BindingData{ diagnostic = diagnostics.no_diagnostic }
}
#! links a file-local import to a package root; failed loads retain their diagnostic.
Import :: struct {
node ast.NodeId
name strpool.StringId
target ?ast.NodeId
diagnostic ?diagnostics.DiagnosticId = null
}
@hide:file
Storage :: struct {
bindings []mut Binding
allocator mem.Allocator
}
#! owns binding storage and retains its allocator; release with deinit.
#! node and diagnostic IDs refer to the caller's Tree and diagnostics.Store.
Resolution :: struct { storage Storage }
@hide:file
Scope :: struct {
names hashmap.AutoHashMap(strpool.StringId, Binding)
parent ?@Scope
}
@hide:file
State :: struct {
tree @ast.Tree
diagnostics @mut diagnostics.Store
bindings []mut Binding
builtin_ids []strpool.StringId
allocator mem.Allocator
namespaces hashmap.AutoHashMap(ast.NodeId, Scope)
aliases []mut ast.NodeId
selections arraylist.ArrayList(ast.NodeId)
}
#! borrows `tree`, its identifier `pool`, and `diagnostic_store` during resolution.
#! `allocator` supplies caller-owned bindings and temporary scopes released before return.
#! release the returned Resolution with deinit, which uses the retained allocator.
resolve proc(
allocator mem.Allocator,
tree @ast.Tree,
pool @mut strpool.StringPool,
diagnostic_store @mut diagnostics.Store,
) Resolution ! (
mem.AllocError
| strpool.InternError
| diagnostics.CapacityError
) {
roots [1]ast.NodeId :: [tree.root]
return try resolve_packages(
allocator,
tree,
roots[..],
&[],
pool,
diagnostic_store,
)
}
#! resolves each package root in its own namespace; AST IDs share the compilation arena.
resolve_packages proc(
allocator mem.Allocator,
tree @ast.Tree,
roots []ast.NodeId,
imports []Import,
pool @mut strpool.StringPool,
diagnostic_store @mut diagnostics.Store,
) Resolution ! (mem.AllocError | strpool.InternError | diagnostics.CapacityError) {
bindings :: try mem.alloc(Binding, allocator, ast.node_count(tree))
errdefer mem.free(allocator, bindings)
mem.set(bindings, Binding{})
builtin_ids :: try intern_builtins(pool, &builtins)
aliases :: try mem.alloc(ast.NodeId, allocator, ast.node_count(tree))
defer mem.free(allocator, aliases)
mem.set(aliases, maxval!(ast.NodeId))
state := State{
tree = tree,
diagnostics = diagnostic_store,
bindings = bindings,
builtin_ids = builtin_ids[..],
allocator = allocator,
namespaces = hashmap.init(allocator),
aliases = aliases,
selections = arraylist.init(allocator),
}
defer arraylist.deinit(&state.selections)
defer {
for (state.namespaces.entries) |entry| {
if (entry.hash == 0) continue
scope := entry.value
hashmap.deinit(&scope.names)
}
hashmap.deinit(&state.namespaces)
}
package_scopes :: try mem.alloc(Scope, allocator, roots.len)
defer mem.free(allocator, package_scopes)
file_scopes :: try mem.alloc(Scope, allocator, ast.source_files(tree).len)
defer mem.free(allocator, file_scopes)
for (file_scopes) |@scope| scope^ = Scope{ names = hashmap.init(allocator), parent = null }
defer { for (file_scopes) |@scope| hashmap.deinit(&scope.names) }
for (roots) |root, index| {
package_scopes[index] = Scope{ names = hashmap.init(allocator), parent = null }
scope := &package_scopes[index]
errdefer hashmap.deinit(&scope.names)
for (0..ast.package_decl_count_at(tree, root)) |i| {
id :: ast.package_decl_at(tree, root, i)
file_scopes[usize(ast.node_span(tree, id).source)].parent = scope
if (ast.file_local(tree, id)) continue
match ast.decl_kind(tree, id) {
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
.import, .invalid: {}
}
}
hashmap.put(&state.namespaces, root, scope^) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
}
for (roots) |root| {
for (0..ast.package_decl_count_at(tree, root)) |i| {
id :: ast.package_decl_at(tree, root, i)
if (!ast.file_local(tree, id)) continue
scope :: &file_scopes[usize(ast.node_span(tree, id).source)]
match ast.decl_kind(tree, id) {
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
.import, .invalid: {}
}
}
}
for (imports) |imported| try resolve_import(&state, &file_scopes[usize(ast.node_span(tree, imported.node).source)], imported)
for (roots) |root| try resolve_package(&state, root, file_scopes)
try canonicalize_aliases(&state)
return Resolution{ storage = Storage{
bindings = bindings,
allocator = allocator,
} }
}
@hide:file
resolve_package proc(
state @mut State, root ast.NodeId, file_scopes []Scope,
) void ! (mem.AllocError | diagnostics.CapacityError) {
tree := state.tree^
tree.root = root
for (0..ast.package_decl_count(&tree)) |i| {
id :: ast.package_decl(&tree, i)
scope :: &file_scopes[usize(ast.node_span(&tree, id).source)]
match ast.decl_kind(&tree, id) {
.statement: try resolve_statement_decl(state, scope, id)
.function: try resolve_function(state, scope, id)
.invalid: resolve_invalid(state, id)
.import: {}
}
}
}
@hide:file
resolve_import proc(
state @mut State, scope @mut Scope, imported Import,
) void ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.import_decl(state.tree, imported.node).span
value := if (imported.diagnostic) |diagnostic|
Binding{ kind = .invalid, data = BindingData{ diagnostic = diagnostic } }
else if (imported.target) |target|
Binding{ kind = .namespace, data = BindingData{ namespace = target } }
else
Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(state.diagnostics, .package_unavailable, span),
} }
if (imported.name == strpool.no_str) {
state.bindings[usize(imported.node)] = value
return
}
if hashmap.get(&scope.names, imported.name) |_| {
state.bindings[usize(imported.node)] = Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(state.diagnostics, .duplicate_name, span),
} }
return
}
if lookup(state, scope.parent, imported.name) |_| {
value = Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(state.diagnostics, .duplicate_name, span),
} }
}
try put_name(scope, imported.name, value)
state.bindings[usize(imported.node)] = value
}
#! releases binding storage with the allocator retained by resolve.
deinit proc(resolution @mut Resolution) void {
mem.free(resolution.storage.allocator, resolution.storage.bindings)
}
binding proc(resolution @Resolution, id ast.NodeId) Binding {
debug.assert(usize(id) < resolution.storage.bindings.len)
return resolution.storage.bindings[usize(id)]
}
#! replaces owned binding storage after AST elaboration; takes ownership of `bindings`.
replace_bindings proc(resolution @mut Resolution, bindings []mut Binding) void {
mem.free(resolution.storage.allocator, resolution.storage.bindings)
resolution.storage.bindings = bindings
}
@hide:file
lookup proc(state @State, scope ?@Scope, name strpool.StringId) ?Binding {
if scope |current| {
if hashmap.get(&current.names, name) |found| return found
return lookup(state, current.parent, name)
}
for (state.builtin_ids) |id, index| {
if (id == name) return Binding{
kind = .builtin,
data = BindingData{ builtin = builtins[index].type },
}
}
return null
}
@hide:file
put_name proc(scope @mut Scope, name strpool.StringId, value Binding) void ! mem.AllocError {
hashmap.put(&scope.names, name, value) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
}
@hide:file
predeclare proc(
state @mut State,
scope @mut Scope,
id ast.NodeId,
name_span ast.Span,
) void ! (mem.AllocError | diagnostics.CapacityError) {
name :: ast.name_id(state.tree, id)
if (mem.eql(ast.source_text(state.tree, name_span), "_")) {
state.bindings[usize(id)] = Binding{ kind = .declaration, data = BindingData{ declaration = id } }
return
}
if hashmap.get(&scope.names, name) |_| {
diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span)
state.bindings[usize(id)] = Binding{
kind = .invalid,
data = BindingData{ diagnostic = diagnostic },
}
return
}
value := Binding{ kind = .declaration, data = BindingData{ declaration = id } }
if lookup(state, scope.parent, name) |_| {
diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span)
# poison the inner name rather than silently binding uses to its ancestor.
value = Binding{ kind = .invalid, data = BindingData{ diagnostic = diagnostic } }
}
try put_name(scope, name, value)
state.bindings[usize(id)] = value
}
@hide:file
resolve_function proc(
state @mut State,
package_scope @Scope,
id ast.NodeId,
) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
function :: ast.function_decl(tree, id)
# compile-time parameters are in scope for subsequent dependent annotations.
signature := Scope{ names = hashmap.init(state.allocator), parent = package_scope }
defer hashmap.deinit(&signature.names)
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
try resolve_expr(state, &signature, ast.param(tree, parameter).type)
if (ast.param(tree, parameter).comptime) try predeclare(state, &signature, parameter, ast.param(tree, parameter).name)
}
try resolve_expr(state, &signature, function.return_type)
scope := Scope{
names = hashmap.init(state.allocator),
parent = package_scope,
}
defer hashmap.deinit(&scope.names)
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
try predeclare(state, &scope, parameter, ast.param(tree, parameter).name)
}
try resolve_block(state, &scope, function.body)
}
@hide:file
resolve_arm proc(state @mut State, parent @Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
scope := Scope{
names = hashmap.init(state.allocator),
parent = parent,
}
defer hashmap.deinit(&scope.names)
try resolve_block(state, &scope, id)
}
@hide:file
resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
if (ast.stmt_kind(tree, statement) == .declaration) try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
}
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
match ast.stmt_kind(tree, statement) {
.declaration: try resolve_statement_decl(state, scope, statement)
.expression: try resolve_expr(state, scope, ast.statement_expr(tree, statement))
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
}
.yield_stmt: try resolve_expr(state, scope, ast.yield_stmt(tree, statement).value?)
.break_stmt, .continue_stmt, .unreachable_stmt: {}
.block_stmt: try resolve_arm(state, scope, ast.statement_block(tree, statement))
.defer_stmt, .errdefer_stmt: {
cleanup :: ast.cleanup_stmt(tree, statement)
try resolve_capture_arm(state, scope, cleanup.body, cleanup.capture)
}
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
try resolve_expr(state, scope, conditional.condition)
try resolve_capture_arm(state, scope, conditional.then_block, conditional.capture)
if (conditional.else_block) |other| try resolve_arm(state, scope, other)
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(tree, statement)
try resolve_expr(state, scope, assignment.target)
try resolve_expr(state, scope, assignment.value)
}
.while_stmt: {
loop :: ast.while_stmt(tree, statement)
try resolve_expr(state, scope, loop.condition)
try resolve_capture_arm(state, scope, loop.body, loop.capture)
}
.for_stmt: {
loop :: ast.for_stmt(tree, statement)
try resolve_expr(state, scope, loop.iterable)
body_scope := Scope{
names = hashmap.init(state.allocator),
parent = scope,
}
defer hashmap.deinit(&body_scope.names)
if (loop.capture) |capture| try predeclare(state, &body_scope, capture, ast.node_span(tree, capture))
if (loop.index) |index| try predeclare(state, &body_scope, index, ast.node_span(tree, index))
try resolve_block(state, &body_scope, loop.body)
}
.invalid: resolve_invalid(state, statement)
}
}
}
@hide:file
resolve_capture_arm proc(state @mut State, scope @Scope, block ast.NodeId, capture ?ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
child := Scope{ names = hashmap.init(state.allocator), parent = scope }
defer hashmap.deinit(&child.names)
if (capture) |id| try predeclare(state, &child, id, ast.node_span(state.tree, id))
try resolve_block(state, &child, block)
}
@hide:file
resolve_statement_decl proc(
state @mut State,
scope @Scope,
id ast.NodeId,
) void ! (mem.AllocError | diagnostics.CapacityError) {
declaration :: ast.statement_decl(state.tree, id)
if (ast.expr_kind(state.tree, declaration.initializer) == .alias_expr) state.aliases[usize(id)] = ast.type_operand(state.tree, declaration.initializer)
if (declaration.type) |type| try resolve_expr(state, scope, type)
try resolve_expr(state, scope, declaration.initializer)
}
@hide:file
canonical_binding proc(state @State, original Binding) Binding {
value := original
for (0..(state.aliases.len)) |_| {
if (value.kind != .declaration) break
operand :: state.aliases[usize(value.data.declaration)]
if (operand == maxval!(ast.NodeId)) break
target :: state.bindings[usize(operand)]
if (target.kind == .none) break
if (target.kind == .declaration and target.data.declaration == value.data.declaration) break
value = target
}
return value
}
@hide:file
canonicalize_aliases proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) {
# aliases may forward-reference other aliases or imported namespace members.
for (0..(state.aliases.len)) |_| {
changed := false
for (state.bindings) |@entry| {
value :: canonical_binding(state, entry^)
if (value.kind != entry.kind) changed = true
else if (value.kind == .declaration and value.data.declaration != entry.data.declaration) changed = true
entry^ = value
}
for (state.selections.items) |id| {
expression :: ast.selection_expr(state.tree, id)
base :: state.bindings[usize(expression.base)]
if (base.kind != .namespace or state.bindings[usize(id)].kind != .none) continue
namespace :: hashmap.get(&state.namespaces, base.data.namespace)?
found :: if (ast.expr_kind(state.tree, expression.member) == .identifier)
hashmap.get(&namespace.names, ast.name_id(state.tree, expression.member))
else
null
value :: if (found) |member| canonical_binding(state, member) else Binding{
kind = .invalid,
data = BindingData{ diagnostic = try diagnostics.add(
state.diagnostics, .unknown_name, ast.node_span(state.tree, expression.member),
) },
}
state.bindings[usize(id)] = value
state.bindings[usize(expression.member)] = value
changed = true
}
if (!changed) break
}
}
@hide:file
resolve_invalid proc(state @mut State, id ast.NodeId) void {
if (state.bindings[usize(id)].kind != .none) return
state.bindings[usize(id)] = Binding{
kind = .invalid,
data = BindingData{ diagnostic = ast.invalid(state.tree, id).diagnostic },
}
}
@hide:file
resolve_expr proc(
state @mut State,
scope @Scope,
id ast.NodeId,
) void ! (mem.AllocError | diagnostics.CapacityError) {
# grouped parameters share their type node, including any diagnostic it owns.
if (state.bindings[usize(id)].kind != .none) return
tree :: state.tree
match ast.expr_kind(tree, id) {
.comptime_expr: try resolve_expr(state, scope, ast.comptime_operand(tree, id))
.block_expr: try resolve_arm(state, scope, ast.block_expr(tree, id))
.typed_constant: {
value :: ast.typed_constant(tree, id)
try resolve_expr(state, scope, value.type)
try resolve_expr(state, scope, value.value)
}
.union_constant: {
value :: ast.union_constant(tree, id)
try resolve_expr(state, scope, value.type)
try resolve_expr(state, scope, value.value)
}
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void,
.literal_undefined: {}
.enum_literal: {}
.distinct_type, .alias_expr, .union_tag_type, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
.enum_type: {
if (ast.enum_type(tree, id).backing) |backing| try resolve_expr(state, scope, backing)
for (0..ast.enum_member_count(tree, id)) |i| {
member :: ast.field(tree, ast.enum_member(tree, id, i))
if (member.value) |value| try resolve_expr(state, scope, value)
}
}
.match_expr: {
try resolve_expr(state, scope, ast.match_expr(tree, id).subject)
for (0..ast.match_arm_count(tree, id)) |i| {
arm_id :: ast.match_arm(tree, id, i)
arm :: ast.match_arm_view(tree, arm_id)
for (0..ast.match_pattern_count(tree, arm_id)) |j| {
try resolve_expr(state, scope, ast.match_pattern(tree, arm_id, j))
}
arm_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
defer hashmap.deinit(&arm_scope.names)
if (arm.capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture))
if (arm.tag_capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture))
if (arm.expression) try resolve_expr(state, &arm_scope, arm.body)
else try resolve_block(state, &arm_scope, arm.body)
}
}
.catch_expr: {
expression :: ast.catch_expr(tree, id)
try resolve_expr(state, scope, expression.operand)
handler_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
defer hashmap.deinit(&handler_scope.names)
if (expression.capture) |capture| try predeclare(state, &handler_scope, capture, ast.node_span(tree, capture))
if (expression.handler_is_block) try resolve_block(state, &handler_scope, expression.handler)
else try resolve_expr(state, &handler_scope, expression.handler)
}
.identifier: {
span :: ast.node_span(tree, id)
name :: ast.name_id(tree, id)
if lookup(state, scope, name) |found| {
state.bindings[usize(id)] = found
} else {
diagnostic :: try diagnostics.add(state.diagnostics, .unknown_name, span)
state.bindings[usize(id)] = Binding{
kind = .invalid,
data = BindingData{ diagnostic = diagnostic },
}
}
}
.selection: {
expression :: ast.selection_expr(tree, id)
try arraylist.append(&state.selections, id)
try resolve_expr(state, scope, expression.base)
base :: state.bindings[usize(expression.base)]
value := Binding{}
if (base.kind == .invalid) {
value = base
} else if (base.kind == .namespace) {
namespace :: hashmap.get(&state.namespaces, base.data.namespace)?
found :: if (ast.expr_kind(tree, expression.member) == .identifier)
hashmap.get(&namespace.names, ast.name_id(tree, expression.member))
else
null
if (found) |member| {
value = member
} else {
value = Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(
state.diagnostics,
.unknown_name,
ast.node_span(tree, expression.member),
),
} }
}
}
state.bindings[usize(id)] = value
state.bindings[usize(expression.member)] = value
}
.unary: try resolve_expr(state, scope, ast.unary_expr(tree, id).operand)
.binary, .fallible_type, .sum_type: {
expression :: ast.binary_expr(tree, id)
try resolve_expr(state, scope, expression.left)
try resolve_expr(state, scope, expression.right)
}
.range: {
expression :: ast.range_expr(tree, id)
try resolve_expr(state, scope, expression.start)
try resolve_expr(state, scope, expression.end)
}
.call: {
try resolve_expr(state, scope, ast.call_expr(tree, id).callee)
for (0..ast.call_arg_count(tree, id)) |i| {
try resolve_expr(state, scope, ast.call_arg(tree, id, i))
}
}
.builtin_call: {
for (0..ast.call_arg_count(tree, id)) |i| try resolve_expr(state, scope, ast.call_arg(tree, id, i))
}
.slice: {
expression :: ast.slice_expr(tree, id)
try resolve_expr(state, scope, expression.base)
if (expression.start) |node| try resolve_expr(state, scope, node)
if (expression.end) |node| try resolve_expr(state, scope, node)
if (expression.sentinel) |node| try resolve_expr(state, scope, node)
}
.slice_type: {
type :: ast.slice_type(tree, id)
try resolve_expr(state, scope, type.element)
if (type.sentinel) |node| try resolve_expr(state, scope, node)
}
.pointer_type: try resolve_expr(state, scope, ast.pointer_type(tree, id).element)
.function_type: {
try resolve_expr(state, scope, ast.callable_result(tree, id))
for 0..ast.function_param_count(tree, id) |i| {
parameter :: ast.function_param(tree, id, i)
try resolve_expr(state, scope, ast.param(tree, parameter).type)
}
}
.array_type: {
type :: ast.array_type(tree, id)
if (type.count) |count| try resolve_expr(state, scope, count)
try resolve_expr(state, scope, type.element)
}
.array_literal: {
for (0..ast.array_element_count(tree, id)) |i| {
try resolve_expr(state, scope, ast.array_element(tree, id, i))
}
}
.record_type, .record_literal, .union_type: {
if (ast.expr_kind(tree, id) == .record_literal) {
if (ast.record_literal(tree, id).type) |type| try resolve_expr(state, scope, type)
}
if (ast.expr_kind(tree, id) == .union_type) {
if (ast.union_type(tree, id).tag) |tag| try resolve_expr(state, scope, tag)
}
for (0..ast.record_field_count(tree, id)) |i| {
field :: ast.field(tree, ast.record_field(tree, id, i))
if (field.type) |type| try resolve_expr(state, scope, type)
if (field.value) |value| try resolve_expr(state, scope, value)
}
}
.index: {
expression :: ast.index_expr(tree, id)
try resolve_expr(state, scope, expression.base)
try resolve_expr(state, scope, expression.index)
}
.invalid: resolve_invalid(state, id)
}
}