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

2516 lines
125 KiB
Plaintext

import "@std/debug"
import "@std/mem"
import "@std/arraylist"
import "@std/strconv"
import "@std/enummap"
import "@ffi/c"
import "@source/ast"
import "@source/diagnostics"
import "@source/ir"
sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
@hide:file
StorageKind :: enum { none, function, global, local }
@hide:file
Storage :: struct {
kind StorageKind = .none
index u32 = 0
}
@hide:file
LoopTargets :: struct {
continue_target ir.BlockId
exit ir.BlockId
cleanup_depth usize
break_reached bool = false
}
@hide:file
ValueTarget :: struct {
join ir.BlockId
slot u32
type typecheck.Type
cleanup_depth usize
reached bool = false
}
@hide:file
State :: struct {
tree @ast.Tree
resolution @sema.Resolution
checked @typecheck.Result
diagnostics @mut diagnostics.Store
module @mut ir.Module
storage []mut Storage
arrays []mut ir.ArrayId
arguments arraylist.ArrayList(ir.ValueId)
cleanups arraylist.ArrayList(ast.NodeId)
records []mut ir.ArrayId
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
loop ?LoopTargets = null
value_target ?@mut ValueTarget = null
return_type typecheck.Type = typecheck.Type{}
fallthrough_block ?ast.NodeId = null
fallthrough_diagnostic ?diagnostics.DiagnosticId = null
}
#! lowers checked source into caller-owned executable storage; release with ir.deinit.
#! no frontend storage or source bytes are retained by the returned module.
#! retains `allocator` in the module; temporary lowering storage is released before returning.
lower proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
return try lower_package(
allocator,
tree,
resolution,
checked,
tree.root,
diagnostic_store,
)
}
#! lowers the module arena, selecting an entry only from `root_package`.
#! retains `allocator` in the returned module; release it with ir.deinit; temporary lowering storage is released before returning.
lower_package proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
root_package ast.NodeId,
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
return try lower_module(
allocator,
tree,
resolution,
checked,
root_package,
null,
null,
diagnostic_store,
)
}
#! lowers an isolated compile-time call wrapper without applying program entry-name rules.
#! the caller supplies a checked parameterless function, an empty `functions` list and owns the returned IR.
#! fills `functions` by IR FunctionId; global initializers map to maxval!(ast.NodeId).
#! retains `allocator` in the returned module; release it with ir.deinit; temporary lowering storage is released before returning.
lower_evaluation proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
entry ast.NodeId,
functions @mut arraylist.ArrayList(ast.NodeId),
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
return try lower_module(
allocator,
tree,
resolution,
checked,
tree.root,
entry,
functions,
diagnostic_store,
)
}
@hide:file
lower_module proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
root_package ast.NodeId,
entry ?ast.NodeId,
functions ?@mut arraylist.ArrayList(ast.NodeId),
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
module := ir.init(allocator)
errdefer ir.deinit(&module)
storage :: try mem.alloc(Storage, allocator, ast.node_count(tree))
defer mem.free(allocator, storage)
mem.set(storage, Storage{})
arrays :: try mem.alloc(ir.ArrayId, allocator, typecheck.array_type_count(checked))
defer mem.free(allocator, arrays)
mem.set(arrays, ir.no_array)
records :: try mem.alloc(ir.ArrayId, allocator, typecheck.record_type_count(checked))
defer mem.free(allocator, records)
mem.set(records, ir.no_array)
state := State{
tree = tree,
resolution = resolution,
checked = checked,
diagnostics = diagnostic_store,
module = &module,
storage = storage,
arrays = arrays,
records = records,
arguments = arraylist.init(ir.ValueId, allocator),
cleanups = arraylist.init(ast.NodeId, allocator),
}
defer arraylist.deinit(&state.arguments)
defer arraylist.deinit(&state.cleanups)
# allocate every package identity before lowering any initializer or body.
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: {
if (ast.function_external(tree, id) and typecheck.type_of(checked, id).kind == .none) continue
function :: try add_function(&module, ast.node_span(tree, id))
if (functions) |declarations| try arraylist.append(declarations, id)
storage[usize(id)] = Storage{ kind = .function, index = u32(function) }
}
.statement: {
if (typecheck.type_of(checked, id).kind == .type_value) continue
if (ast.expr_kind(tree, ast.statement_decl(tree, id).initializer) == .alias_expr) continue
initializer :: try add_function(&module, ast.node_span(tree, id))
if (functions) |declarations| try arraylist.append(declarations, maxval!(ast.NodeId))
global :: u32(module.globals.items.len)
try arraylist.append(&module.globals, ir.Global{
type = lower_type(&state, typecheck.type_of(checked, id)),
array_type = try array_shape(&state, typecheck.type_of(checked, id)),
initializer = initializer,
span = ast.node_span(tree, id),
})
storage[usize(id)] = Storage{ kind = .global, index = global }
}
.import, .invalid: {}
}
}
if (entry) |function| module.entry = ir.FunctionId(storage[usize(function)].index)
else try find_entry(&state, root_package)
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: {
if (storage[usize(id)].kind == .function) try lower_function(&state, id)
}
.statement: {
if (ast.expr_kind(tree, ast.statement_decl(tree, id).initializer) == .alias_expr) continue
if (typecheck.type_of(checked, id).kind != .type_value) try lower_global(&state, id)
}
.import, .invalid: {}
}
}
return module
}
@hide:file
base_types :: $enummap.init_default(
typecheck.TypeKind, ir.Type, ir.Type.invalid, {
int = .int,
usize = .usize,
i8 = .i8,
i16 = .i16,
i32 = .i32,
isize = .isize,
u8 = .u8,
u16 = .u16,
u32 = .u32,
u64 = .u64,
array = .array,
record = .record,
pointer = .pointer,
float = .float,
bool = .bool,
string = .string,
void = .void,
function = .function,
none = .invalid,
invalid = .invalid,
range = .invalid,
type_value = .invalid,
null = .invalid,
anyopaque = .invalid,
},
)
@hide:file
lower_type proc(state @State, type typecheck.Type) ir.Type {
if (type.kind == .enumeration) return lower_type(state, typecheck.Type{ kind = typecheck.enum_type(state.checked, type.enum_type).backing })
if (type.kind == .slice) return .record
if (type.kind == .fallible) return .record
if (type.kind == .optional) return lower_optional_type(state, typecheck.array_type(state.checked, type.array_type).element)
return enummap.get(&base_types, type.kind)?
}
@hide:file
lower_optional_type proc(state @State, child typecheck.Type) ir.Type {
if (child.kind == .pointer or child.kind == .function) return lower_type(state, child)
return .record
}
@hide:file
array_shape proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (type.kind == .record) return try prepare_record(state, type.record_type)
if (type.kind == .slice or type.kind == .optional or type.kind == .fallible) return try prepare_container(state, type)
if (type.kind != .array) return ir.no_array
# checker history includes unresolved shapes; materialize only final used types.
return try prepare_array(state, type.array_type)
}
@hide:file
prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (state.arrays[usize(id)] != ir.no_array) return state.arrays[usize(id)]
descriptor :: typecheck.array_type(state.checked, id)
child := ir.no_array
child = try array_shape(state, descriptor.element)
result :: try ir.add_array(
state.module,
lower_type(state, descriptor.element),
child,
descriptor.len,
descriptor.mutable,
)
state.arrays[usize(id)] = result
return result
}
@hide:file
prepare_container proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
descriptor :: typecheck.array_type(state.checked, type.array_type)
if (type.kind == .optional and (descriptor.element.kind == .pointer or descriptor.element.kind == .function)) return ir.no_array
index :: usize(type.array_type)
if (state.arrays[index] != ir.no_array) return state.arrays[index]
if (type.kind == .fallible) {
fields [2]ir.RecordField :: [
{
type = lower_type(state, descriptor.element),
array_type = try array_shape(state, descriptor.element),
tag = ir.Value{ type = .int, data = ir.ValueData{ integer = 0 } },
},
{
type = lower_type(state, descriptor.error_type),
array_type = try array_shape(state, descriptor.error_type),
tag = ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } },
},
]
shape :: try ir.add_union(state.module, fields[..], true)
state.arrays[index] = shape
return shape
}
fields [2]mut ir.RecordField := [{ type = .pointer }, { type = .usize }]
if (type.kind == .optional) {
fields[0] = ir.RecordField{ type = .bool }
fields[1] = ir.RecordField{ type = lower_type(state, descriptor.element), array_type = try array_shape(state, descriptor.element) }
}
shape :: try ir.add_record(state.module, fields[..])
state.arrays[index] = shape
return shape
}
@hide:file
prepare_record proc(state @mut State, id typecheck.RecordId) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (state.records[usize(id)] != ir.no_array) return state.records[usize(id)]
descriptor :: typecheck.record_type(state.checked, id)
fields := arraylist.init(ir.RecordField, state.module.allocator)
defer arraylist.deinit(&fields)
try arraylist.reserve(&fields, descriptor.fields.len)
for (descriptor.fields) |field| {
tag := ir.Value{}
if (descriptor.is_union and descriptor.tag_type.kind == .enumeration) {
members :: typecheck.enum_type(state.checked, descriptor.tag_type.enum_type).members
for members |member, index| {
if (ast.name_id(state.tree, member.name) == ast.name_id(state.tree, field.name?)) {
tag = enum_value(state, descriptor.tag_type, index)
break
}
}
}
try arraylist.append(&fields, ir.RecordField{
type = lower_type(state, field.type),
array_type = try array_shape(state, field.type),
tag = tag,
})
}
shape :: if (descriptor.is_union) try ir.add_union(state.module, fields.items, descriptor.tag_type.kind == .enumeration)
else try ir.add_record(state.module, fields.items)
state.records[usize(id)] = shape
return shape
}
@hide:file
add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError {
id :: ir.FunctionId(module.functions.items.len)
try arraylist.append(&module.functions, ir.init_function(module.allocator))
function := &module.functions.items[usize(id)]
function.span = span
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
return id
}
@hide:file
find_entry proc(state @mut State, root_package ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
package_tree := state.tree^
package_tree.root = root_package
tree :: &package_tree
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
name := diagnostics.Span{ start = 0, end = 0 }
match ast.decl_kind(tree, id) {
.function: name = ast.function_decl(tree, id).name
.statement: name = ast.statement_decl(tree, id).name
.import, .invalid: continue
}
if (!mem.eql(ast.source_text(tree, name), "main")) continue
if (ast.decl_kind(tree, id) != .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return
}
declaration :: ast.function_decl(tree, id)
result :: lower_type(state, typecheck.type_of(state.checked, declaration.return_type))
if (ast.function_param_count(tree, id) != 0 or result == .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return
}
state.module.entry = ir.FunctionId(state.storage[usize(id)].index)
return
}
state.module.entry_diagnostic = try diagnostics.add(
state.diagnostics,
.missing_entry,
ast.node_span(tree, root_package),
)
}
@hide:file
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .trap,
diagnostic = diagnostic,
span = span,
}
state.terminated = true
}
@hide:file
trap_type proc(state @mut State, id ast.NodeId) bool {
type :: typecheck.type_of(state.checked, id)
if (type.kind != .invalid) return false
trap(state, type.diagnostic, ast.node_span(state.tree, id))
return true
}
@hide:file
trap_binding proc(state @mut State, id ast.NodeId) bool {
binding :: sema.binding(state.resolution, id)
if (binding.kind != .invalid) return false
trap(state, binding.data.diagnostic, ast.node_span(state.tree, id))
return true
}
@hide:file
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
value = value,
span = span,
}
state.terminated = true
}
@hide:file
emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
return try ir.emit(state.module, ir.FunctionId(state.function), state.block, instruction)
}
@hide:file
constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
return try emit(state, ir.Instruction{ op = .constant, type = value.type, constant = value, span = span })
}
@hide:file
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
state.function = usize(global.initializer)
state.block = ir.BlockId(0)
state.terminated = false
state.module.functions.items[state.function].result = global.type
state.module.functions.items[state.function].result_array_type = global.array_type
if (trap_binding(state, id)) return
declaration :: ast.statement_decl(state.tree, id)
if (declaration.type) |annotation| {
if (trap_type(state, annotation)) return
}
value :: try lower_as(state, declaration.initializer, typecheck.type_of(state.checked, id))
if (state.terminated or trap_type(state, id)) return
finish(state, value, declaration.span)
}
@hide:file
lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
declaration :: ast.function_decl(tree, id)
state.function = usize(state.storage[usize(id)].index)
state.block = ir.BlockId(0)
state.terminated = false
state.loop = null
state.value_target = null
state.fallthrough_block = declaration.body
state.fallthrough_diagnostic = typecheck.fallthrough_diagnostic(state.checked, id)
state.return_type = typecheck.type_of(state.checked, declaration.return_type)
state.module.functions.items[state.function].result = lower_type(state, typecheck.type_of(state.checked, declaration.return_type))
state.module.functions.items[state.function].result_array_type = try array_shape(state, typecheck.type_of(state.checked, declaration.return_type))
state.module.functions.items[state.function].result_fallible = state.return_type.kind == .fallible
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
type :: typecheck.type_of(state.checked, parameter)
local :: try ir.add_parameter(
state.module,
ir.FunctionId(state.function),
lower_type(state, type),
try array_shape(state, type),
)
state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(local) }
}
if (ast.function_external(tree, id)) {
if (trap_binding(state, id) or trap_type(state, id)) return
name :: ast.source_text(tree, declaration.name)
owned :: try mem.alloc(u8, state.module.allocator, name.len)
memcopy!(owned, name)
state.module.functions.items[state.function].external_name = owned
return
}
try predeclare_locals(state, declaration.body)
# signature errors are entry traps; body errors are reached in statement order.
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
for (0..ast.function_param_count(tree, id)) |i| {
if (trap_type(state, ast.function_param(tree, id, i))) return
}
if (trap_type(state, id)) return
try lower_block(state, declaration.body, false)
if (state.terminated) return
if (typecheck.fallthrough_diagnostic(state.checked, id)) |diagnostic| {
trap(state, diagnostic, ast.node_span(tree, declaration.body))
return
}
value := ir.no_value
if (state.return_type.kind == .fallible) value = try wrap_fallible(state, value, state.return_type, false, ast.node_span(tree, declaration.body))
finish(state, value, ast.node_span(tree, declaration.body))
}
@hide:file
predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
for (0..ast.block_statement_count(state.tree, block)) |i| {
statement :: ast.block_statement(state.tree, block, i)
match ast.stmt_kind(state.tree, statement) {
.declaration: {
if (typecheck.type_of(state.checked, statement).kind == .type_value) continue
if (ast.expr_kind(state.tree, ast.statement_decl(state.tree, statement).initializer) == .alias_expr) continue
type :: typecheck.type_of(state.checked, statement)
local :: try ir.add_local(
state.module,
ir.FunctionId(state.function),
lower_type(state, type),
try array_shape(state, type),
)
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(local) }
}
.if_stmt: {
conditional :: ast.if_stmt(state.tree, statement)
if (conditional.capture) |capture| try predeclare_capture(state, capture)
try predeclare_locals(state, conditional.then_block)
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
}
.while_stmt: {
loop :: ast.while_stmt(state.tree, statement)
if (loop.capture) |capture| try predeclare_capture(state, capture)
try predeclare_locals(state, loop.body)
}
.for_stmt: {
loop :: ast.for_stmt(state.tree, statement)
is_range :: ast.expr_kind(state.tree, loop.iterable) == .range
family :: typecheck.type_of(state.checked, loop.iterable).range_element
type ir.Type :: if (is_range) lower_type(state, typecheck.Type{ kind = family }) else .usize
slot :: try ir.add_local(state.module, ir.FunctionId(state.function), type, ir.no_array)
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(slot) }
if (loop.capture) |capture| {
if (is_range) state.storage[usize(capture)] = state.storage[usize(statement)]
else try predeclare_capture(state, capture)
}
if (loop.index) |index| {
index_slot :: try ir.add_local(state.module, ir.FunctionId(state.function), .usize, ir.no_array)
state.storage[usize(index)] = Storage{ kind = .local, index = u32(index_slot) }
}
try predeclare_locals(state, loop.body)
}
.block_stmt: try predeclare_locals(state, ast.statement_block(state.tree, statement))
.defer_stmt, .errdefer_stmt: {
cleanup :: ast.cleanup_stmt(state.tree, statement)
if (cleanup.capture) |capture| try predeclare_capture(state, capture)
try predeclare_locals(state, cleanup.body)
}
else: {}
}
}
}
@hide:file
predeclare_capture proc(state @mut State, capture ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
type :: typecheck.type_of(state.checked, capture)
slot :: try ir.add_local(state.module, ir.FunctionId(state.function), lower_type(state, type), try array_shape(state, type))
state.storage[usize(capture)] = Storage{ kind = .local, index = u32(slot) }
}
@hide:file
store_capture proc(state @mut State, capture ast.NodeId, value ir.ValueId, optional_type typecheck.Type, span diagnostics.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
payload :: try optional_payload(state, value, optional_type, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = payload, slot = state.storage[usize(capture)].index, span = span })
}
@hide:file
lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (mem.AllocError | diagnostics.CapacityError) {
cleanup_depth :: state.cleanups.items.len
defer state.cleanups.items = state.cleanups.items.ptr[..cleanup_depth]
# reset every direct declaration before control can return or trap within the scope.
if (fresh_scope) {
for (0..ast.block_statement_count(state.tree, block)) |i| {
declaration :: ast.block_statement(state.tree, block, i)
if (ast.stmt_kind(state.tree, declaration) != .declaration) continue
if (typecheck.type_of(state.checked, declaration).kind == .type_value) continue
if (ast.expr_kind(state.tree, ast.statement_decl(state.tree, declaration).initializer) == .alias_expr) continue
_ = try emit(state, ir.Instruction{
op = .reset_local,
slot = state.storage[usize(declaration)].index,
span = ast.node_span(state.tree, declaration),
})
}
}
for (0..ast.block_statement_count(state.tree, block)) |i| {
if (state.terminated) return
try lower_statement(state, ast.block_statement(state.tree, block, i))
}
if (!state.terminated) {
if (state.fallthrough_block) |function_body| {
if (function_body == block) {
if (state.fallthrough_diagnostic) |diagnostic| {
trap(state, diagnostic, ast.node_span(state.tree, block))
return
}
}
}
}
if (!state.terminated) try lower_cleanups(state, cleanup_depth, ir.no_value)
}
@hide:file
lower_value_block proc(state @mut State, block, owner ast.NodeId, type typecheck.Type) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, block)
runtime :: lower_type(state, type)
shape :: try array_shape(state, type)
slot := u32(0)
if (runtime != .void) slot = u32(try ir.add_local(state.module, ir.FunctionId(state.function), runtime, shape))
join :: try add_block(state)
reached :: try lower_value_body(state, block, owner, type, slot, join)
state.block = join
state.terminated = !reached
if (!reached) state.module.functions.items[state.function].blocks.items[usize(join)].terminator = ir.Terminator{ kind = .unreachable }
if (!reached or runtime == .void) return ir.no_value
return try emit(state, ir.Instruction{ op = .load_local, type = runtime, array_type = shape, slot = slot, span = span })
}
@hide:file
lower_value_body proc(state @mut State, block, owner ast.NodeId, type typecheck.Type, slot u32, join ir.BlockId) bool ! (mem.AllocError | diagnostics.CapacityError) {
if (trap_type(state, block)) return false
target := ValueTarget{ join = join, slot = slot, type = type, cleanup_depth = state.cleanups.items.len }
previous_target :: state.value_target
state.value_target = &target
defer state.value_target = previous_target
previous_fallthrough :: state.fallthrough_block
previous_diagnostic :: state.fallthrough_diagnostic
state.fallthrough_block = block
state.fallthrough_diagnostic = typecheck.fallthrough_diagnostic(state.checked, owner)
defer state.fallthrough_block = previous_fallthrough
defer state.fallthrough_diagnostic = previous_diagnostic
try predeclare_locals(state, block)
try lower_block(state, block, true)
if (!state.terminated) {
# checking permits an implicit result only for void blocks.
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = ast.node_span(state.tree, block),
}
target.reached = true
state.terminated = true
}
return target.reached
}
@hide:file
lower_cleanups proc(state @mut State, depth usize, error_value ir.ValueId) void ! (mem.AllocError | diagnostics.CapacityError) {
previous_target :: state.value_target
state.value_target = null
defer state.value_target = previous_target
index := state.cleanups.items.len
while (index > depth) {
index -= 1
id :: state.cleanups.items[index]
if (ast.stmt_kind(state.tree, id) == .errdefer_stmt and error_value == ir.no_value) continue
cleanup :: ast.cleanup_stmt(state.tree, id)
if (cleanup.capture) |capture| _ = try emit(state, ir.Instruction{
op = .store_local, left = error_value, slot = state.storage[usize(capture)].index, span = cleanup.span,
})
try lower_block(state, cleanup.body, true)
if (state.terminated) return
}
}
@hide:file
wrap_fallible proc(state @mut State, value ir.ValueId, type typecheck.Type, failed bool, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
slot :: if (failed) u32(1) else u32(0)
shape :: try array_shape(state, type)
return try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = slot,
left = value, constant = state.module.arrays.items[usize(shape)].fields[usize(slot)].tag, span = span,
})
}
@hide:file
fallible_failed proc(state @mut State, value ir.ValueId, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tag :: try emit(state, ir.Instruction{ op = .union_tag, type = .int, left = value, span = span })
error_tag :: try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } }, span)
return try emit(state, ir.Instruction{ op = .equal, type = .bool, left = tag, right = error_tag, span = span })
}
@hide:file
lower_return proc(state @mut State, value ir.ValueId, span diagnostics.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
if (state.return_type.kind != .fallible) {
try lower_cleanups(state, 0, ir.no_value)
if (!state.terminated) finish(state, value, span)
return
}
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
state.block = failure
error_type :: typecheck.array_type(state.checked, state.return_type.array_type).error_type
error_value :: try project(state, value, lower_type(state, error_type), try array_shape(state, error_type), 1, span)
try lower_cleanups(state, 0, error_value)
if (!state.terminated) finish(state, value, span)
state.block = success
state.terminated = false
try lower_cleanups(state, 0, ir.no_value)
if (!state.terminated) finish(state, value, span)
}
@hide:file
add_block proc(state @mut State) ir.BlockId ! mem.AllocError {
function := &state.module.functions.items[state.function]
id :: ir.BlockId(function.blocks.items.len)
try arraylist.append(&function.blocks, ir.init_block(state.module.allocator))
return id
}
@hide:file
lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
conditional :: ast.if_stmt(state.tree, id)
value :: try lower_expression(state, conditional.condition)
if (state.terminated) return
condition := value
if (conditional.capture != null) condition = try optional_present(state, value, typecheck.type_of(state.checked, conditional.condition), conditional.span)
origin :: state.block
consequent :: try add_block(state)
alternative :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .branch,
value = condition,
target = consequent,
alternative = alternative,
span = ast.node_span(state.tree, conditional.condition),
}
state.block = consequent
if (conditional.capture) |capture| try store_capture(state, capture, value, typecheck.type_of(state.checked, conditional.condition), conditional.span)
try lower_block(state, conditional.then_block, true)
consequent_end :: state.block
consequent_falls :: !state.terminated
state.block = alternative
state.terminated = false
if (conditional.else_block) |block| try lower_block(state, block, true)
alternative_end :: state.block
alternative_falls :: !state.terminated
if (!consequent_falls and !alternative_falls) {
state.terminated = true
return
}
join :: try add_block(state)
if (consequent_falls) {
state.module.functions.items[state.function].blocks.items[usize(consequent_end)].terminator = ir.Terminator{
kind = .jump, target = join, span = conditional.span,
}
}
if (alternative_falls) {
state.module.functions.items[state.function].blocks.items[usize(alternative_end)].terminator = ir.Terminator{
kind = .jump, target = join, span = conditional.span,
}
}
state.block = join
state.terminated = false
}
@hide:file
lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
loop :: ast.while_stmt(state.tree, id)
origin :: state.block
guard :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .jump, target = guard, span = loop.span,
}
state.block = guard
value :: try lower_expression(state, loop.condition)
if (state.terminated) return
condition := value
if (loop.capture != null) condition = try optional_present(state, value, typecheck.type_of(state.checked, loop.condition), loop.span)
guard_end :: state.block
body :: try add_block(state)
exit :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(guard_end)].terminator = ir.Terminator{
kind = .branch,
value = condition,
target = body,
alternative = exit,
span = ast.node_span(state.tree, loop.condition),
}
previous_loop :: state.loop
state.loop = LoopTargets{ continue_target = guard, exit = exit, cleanup_depth = state.cleanups.items.len }
defer state.loop = previous_loop
state.block = body
if (loop.capture) |capture| try store_capture(state, capture, value, typecheck.type_of(state.checked, loop.condition), loop.span)
try lower_block(state, loop.body, true)
if (!state.terminated) {
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = guard, span = loop.span,
}
}
state.block = exit
state.terminated = (
loop.capture == null
and ast.expr_kind(state.tree, loop.condition) == .literal_bool
and mem.eql(ast.literal_text(state.tree, loop.condition), "true")
and !state.loop?.break_reached
)
if (state.terminated)
state.module.functions.items[state.function].blocks.items[usize(exit)].terminator = ir.Terminator{ kind = .unreachable, span = loop.span }
}
@hide:file
lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
loop :: ast.for_stmt(state.tree, id)
if (ast.expr_kind(state.tree, loop.iterable) != .range) {
try lower_collection_for(state, id)
return
}
interval :: ast.range_expr(state.tree, loop.iterable)
start :: try lower_expression(state, interval.start)
if (state.terminated) return
end :: try lower_expression(state, interval.end)
if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return
slot :: state.storage[usize(id)].index
family :: lower_type(state, typecheck.Type{ kind = typecheck.type_of(state.checked, loop.iterable).range_element })
_ = try emit(state, ir.Instruction{ op = .store_local, type = family, left = start, slot = slot, span = interval.span })
origin :: state.block
condition :: try add_block(state)
body :: try add_block(state)
update :: try add_block(state)
increment :: try add_block(state)
exit :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .jump, target = condition, span = loop.span,
}
state.block = condition
current :: try emit(state, ir.Instruction{ op = .load_local, type = family, slot = slot, span = interval.span })
comparison ir.Op :: if (interval.inclusive) .less_equal else .less
condition_value :: try emit(state, ir.Instruction{ op = comparison, type = .bool, left = current, right = end, span = interval.span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = condition_value, target = body, alternative = exit, span = interval.span,
}
previous_loop :: state.loop
state.loop = LoopTargets{ continue_target = update, exit = exit, cleanup_depth = state.cleanups.items.len }
defer state.loop = previous_loop
state.block = body
if (loop.capture) |capture| _ = trap_binding(state, capture)
if (!state.terminated) try lower_block(state, loop.body, true)
if (!state.terminated) {
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = update, span = loop.span,
}
}
state.block = update
state.terminated = false
last :: try emit(state, ir.Instruction{ op = .load_local, type = family, slot = slot, span = interval.span })
finished :: try emit(state, ir.Instruction{ op = .equal, type = .bool, left = last, right = end, span = interval.span })
# continue reaches this guard too, so an inclusive maximum never adds one.
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = finished, target = exit, alternative = increment, span = interval.span,
}
state.block = increment
unit := ir.Value{ type = family, data = ir.ValueData{ integer = 1 } }
if (ir.unsigned_integer(family)) unit.data = ir.ValueData{ unsigned = 1 }
one :: try constant(state, unit, interval.span)
next :: try emit(state, ir.Instruction{ op = .add, type = family, left = last, right = one, span = interval.span })
_ = try emit(state, ir.Instruction{ op = .store_local, type = family, left = next, slot = slot, span = interval.span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = condition, span = loop.span,
}
state.block = exit
state.terminated = false
}
@hide:file
lower_collection_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
loop :: ast.for_stmt(state.tree, id)
parts :: try slice_parts(state, loop.iterable)
if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return
slot :: state.storage[usize(id)].index
zero :: try usize_constant(state, 0, loop.span)
_ = try emit(state, ir.Instruction{ op = .store_local, type = .usize, left = zero, slot = slot, span = loop.span })
origin :: state.block
condition :: try add_block(state)
body :: try add_block(state)
update :: try add_block(state)
exit :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .jump, target = condition, span = loop.span,
}
state.block = condition
current :: try emit(state, ir.Instruction{ op = .load_local, type = .usize, slot = slot, span = loop.span })
condition_value :: try emit(state, ir.Instruction{ op = .less, type = .bool, left = current, right = parts.bound, span = loop.span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = condition_value, target = body, alternative = exit, span = loop.span,
}
previous_loop :: state.loop
state.loop = LoopTargets{ continue_target = update, exit = exit, cleanup_depth = state.cleanups.items.len }
defer state.loop = previous_loop
state.block = body
if (loop.capture) |capture| {
if (!trap_binding(state, capture) and !trap_type(state, capture)) {
runtime :: lower_type(state, parts.element)
shape :: try array_shape(state, parts.element)
pointer :: try emit(state, ir.Instruction{
op = .pointer_offset, type = .pointer, left = parts.pointer, right = current,
pointee = runtime, array_type = shape, span = loop.span,
})
value := pointer
if (!loop.pointer_capture) {
address :: try pointer_address(state, pointer, parts.element, loop.span)
value = try emit(state, ir.Instruction{
op = .load_address, type = runtime, array_type = shape, left = address, span = loop.span,
})
}
checked :: typecheck.type_of(state.checked, capture)
_ = try emit(state, ir.Instruction{
op = .store_local, type = lower_type(state, checked), array_type = try array_shape(state, checked),
left = value, slot = state.storage[usize(capture)].index, span = loop.span,
})
}
}
if (loop.index) |index| {
if (!state.terminated and !trap_binding(state, index) and !trap_type(state, index)) {
_ = try emit(state, ir.Instruction{
op = .store_local, type = .usize, left = current, slot = state.storage[usize(index)].index, span = loop.span,
})
}
}
if (!state.terminated) try lower_block(state, loop.body, true)
if (!state.terminated) state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = update, span = loop.span,
}
state.block = update
state.terminated = false
last :: try emit(state, ir.Instruction{ op = .load_local, type = .usize, slot = slot, span = loop.span })
one :: try usize_constant(state, 1, loop.span)
next :: try emit(state, ir.Instruction{ op = .add, type = .usize, left = last, right = one, span = loop.span })
_ = try emit(state, ir.Instruction{ op = .store_local, type = .usize, left = next, slot = slot, span = loop.span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = condition, span = loop.span,
}
state.block = exit
state.terminated = false
}
@hide:file
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
match ast.stmt_kind(tree, id) {
.block_stmt: try lower_block(state, ast.statement_block(tree, id), true)
.defer_stmt, .errdefer_stmt: {
if (trap_type(state, id)) return
try arraylist.append(&state.cleanups, id)
}
.if_stmt: try lower_if(state, id)
.while_stmt: try lower_while(state, id)
.for_stmt: try lower_for(state, id)
.unreachable_stmt: {
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .unreachable, span = ast.node_span(tree, id),
}
state.terminated = true
}
.break_stmt, .continue_stmt: {
if (trap_type(state, id)) return
if (state.loop) |targets| {
target := targets.exit
# each loop supplies its own guard or guarded-update entry.
if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.continue_target
try lower_cleanups(state, targets.cleanup_depth, ir.no_value)
if (state.terminated) return
if (ast.stmt_kind(tree, id) == .break_stmt) {
reached := targets
reached.break_reached = true
state.loop = reached
}
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = target, span = ast.node_span(tree, id),
}
state.terminated = true
} else unreachable
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(tree, id)
if (assignment.compound) {
if (trap_type(state, id)) return
expression :: ast.binary_expr(tree, assignment.value)
address :: try lower_place(state, assignment.target)
if (state.terminated) return
checked :: typecheck.type_of(state.checked, assignment.target)
type :: lower_type(state, checked)
old :: try emit(state, ir.Instruction{
op = .load_address, type = type, left = address,
array_type = try array_shape(state, checked), span = ast.node_span(tree, assignment.target),
})
right :: try lower_expression(state, expression.right)
if (state.terminated) return
value :: if (expression.operator == .add and type == .pointer) {
pointer :: typecheck.pointer_type(state.checked, checked.pointer_type)
yield try emit(state, ir.Instruction{
op = .pointer_offset, type = type, left = old, right = right,
pointee = lower_type(state, pointer.element),
array_type = try array_shape(state, pointer.element), span = ast.node_span(tree, id),
})
} else try emit(state, ir.Instruction{
op = enummap.get(&binary_ops, expression.operator)?, type = type, left = old, right = right,
span = ast.node_span(tree, id),
})
_ = try emit(state, ir.Instruction{
op = .store_address, type = .void, left = address, right = value,
span = ast.node_span(tree, assignment.target),
})
return
}
if (
ast.expr_kind(tree, assignment.target) == .index
or ast.expr_kind(tree, assignment.target) == .unary
or (ast.expr_kind(tree, assignment.target) == .selection
and sema.binding(state.resolution, assignment.target).kind == .none)
) {
value :: try lower_as(state, assignment.value, typecheck.type_of(state.checked, assignment.target))
if (state.terminated) return
if (!is_place(state, assignment.target)) {
_ = try lower_expression(state, assignment.target)
if (!state.terminated) _ = trap_type(state, id)
return
}
address :: try lower_place(state, assignment.target)
if (state.terminated or trap_type(state, id)) return
_ = try emit(state, ir.Instruction{
op = .store_address,
type = .void,
left = address,
right = value,
span = ast.node_span(tree, assignment.target),
})
return
}
if (trap_binding(state, assignment.target) or trap_type(state, id)) return
value :: try lower_as(state, assignment.value, typecheck.type_of(state.checked, assignment.target))
if (state.terminated) return
binding :: sema.binding(state.resolution, assignment.target)
_ = try emit(state, ir.Instruction{
op = .assign_local,
type = lower_type(state, typecheck.type_of(state.checked, assignment.target)),
array_type = try array_shape(state, typecheck.type_of(state.checked, assignment.target)),
left = value,
slot = state.storage[usize(binding.data.declaration)].index,
span = ast.node_span(tree, assignment.target),
})
}
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
}
.expression: _ = try lower_expression(state, ast.statement_expr(tree, id))
.declaration: {
if (typecheck.type_of(state.checked, id).kind == .type_value) return
if (ast.expr_kind(tree, ast.statement_decl(tree, id).initializer) == .alias_expr) return
if (trap_binding(state, id)) return
declaration :: ast.statement_decl(tree, id)
if (declaration.type) |annotation| {
if (trap_type(state, annotation)) return
}
value :: try lower_as(state, declaration.initializer, typecheck.type_of(state.checked, id))
if (state.terminated or trap_type(state, id)) return
_ = try emit(state, ir.Instruction{
op = .store_local,
type = lower_type(state, typecheck.type_of(state.checked, id)),
array_type = try array_shape(state, typecheck.type_of(state.checked, id)),
left = value,
slot = state.storage[usize(id)].index,
span = declaration.span,
})
}
.yield_stmt: {
statement :: ast.yield_stmt(tree, id)
if (state.value_target) |target| {
value :: try lower_as(state, statement.value?, target.type)
if (state.terminated or trap_type(state, id)) return
if (lower_type(state, target.type) != .void) _ = try emit(state, ir.Instruction{
op = .store_local, left = value, slot = target.slot, span = statement.span,
})
try lower_cleanups(state, target.cleanup_depth, ir.no_value)
if (state.terminated) return
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = target.join, span = statement.span,
}
target.reached = true
state.terminated = true
} else if (!trap_type(state, id)) unreachable
}
.return_stmt: {
statement :: ast.return_stmt(tree, id)
value := ir.no_value
if (statement.value) |expression| value = try lower_as(state, expression, state.return_type)
if (state.terminated or trap_type(state, id)) return
if (statement.value == null and state.return_type.kind == .fallible) value = try wrap_fallible(state, value, state.return_type, false, statement.span)
try lower_return(state, value, statement.span)
}
}
}
@hide:file
binary_ops :: $enummap.init_default(
ast.BinaryOperator, ir.Op, ir.Op.constant, {
add = .add,
subtract = .subtract,
multiply = .multiply,
divide = .divide,
bit_and = .bit_and,
bit_or = .bit_or,
bit_xor = .bit_xor,
shift_left = .shift_left,
shift_right = .shift_right,
shift_left_saturating = .shift_left_saturating,
equal = .equal,
not_equal = .not_equal,
less = .less,
less_equal = .less_equal,
greater = .greater,
greater_equal = .greater_equal,
},
)
@hide:file
unary_ops :: $enummap.init_default(
ast.UnaryOperator, ir.Op, ir.Op.constant, {
negate = .negate,
logical_not = .logical_not,
bit_not = .bit_not,
},
)
@hide:file
lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.binary_expr(state.tree, id)
span :: ast.node_span(state.tree, id)
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
origin :: state.block
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .bool, ir.no_array))
right_block :: try add_block(state)
skip :: try add_block(state)
join :: try add_block(state)
conjunction :: expression.operator == .logical_and
target :: if (conjunction) right_block else skip
alternative :: if (conjunction) skip else right_block
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .branch,
value = left,
target = target,
alternative = alternative,
span = ast.node_span(state.tree, expression.left),
}
state.block = skip
_ = try emit(state, ir.Instruction{ op = .store_local, type = .bool, left = left, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = right_block
right :: try lower_expression(state, expression.right)
if (!state.terminated) {
_ = try emit(state, ir.Instruction{ op = .store_local, type = .bool, left = right, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
}
# the skip edge reaches the join even when the demanded right edge traps.
state.block = join
state.terminated = false
if (trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .load_local, type = .bool, slot = slot, span = span })
}
@hide:file
enum_value proc(state @State, type typecheck.Type, index usize) ir.Value {
descriptor :: typecheck.enum_type(state.checked, type.enum_type)
member :: descriptor.members[index]
runtime :: lower_type(state, type)
if (ir.unsigned_integer(runtime)) return ir.Value{ type = runtime, data = ir.ValueData{ unsigned = member.magnitude } }
integer := i64(0)
if (member.negative) {
if (member.magnitude == u64(maxval!(i64)) + 1) integer = minval!(i64)
else integer = -i64(member.magnitude)
} else integer = i64(member.magnitude)
return ir.Value{ type = runtime, data = ir.ValueData{ integer = integer } }
}
@hide:file
match_condition proc(
state @mut State, subject ir.ValueId, pattern ast.NodeId,
) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, pattern)
if (ast.expr_kind(state.tree, pattern) == .range) {
interval :: ast.range_expr(state.tree, pattern)
start :: try lower_expression(state, interval.start)
if (state.terminated) return ir.no_value
end :: try lower_expression(state, interval.end)
if (state.terminated) return ir.no_value
lower_bound :: try emit(state, ir.Instruction{ op = .greater_equal, type = .bool, left = subject, right = start, span = span })
upper_op :: if (interval.inclusive) ir.Op.less_equal else ir.Op.less
upper :: try emit(state, ir.Instruction{ op = upper_op, type = .bool, left = subject, right = end, span = span })
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .bool, ir.no_array))
_ = try emit(state, ir.Instruction{ op = .store_local, left = lower_bound, slot = slot, span = span })
origin :: state.block
check_upper :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .branch, value = lower_bound, target = check_upper, alternative = join, span = span,
}
state.block = check_upper
_ = try emit(state, ir.Instruction{ op = .store_local, left = upper, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .bool, slot = slot, span = span })
}
value :: try lower_expression(state, pattern)
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{ op = .equal, type = .bool, left = subject, right = value, span = span })
}
@hide:file
lower_match proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
span :: ast.node_span(tree, id)
view :: ast.match_expr(tree, id)
subject :: try lower_expression(state, view.subject)
if (state.terminated or trap_type(state, id)) return ir.no_value
subject_type :: typecheck.type_of(state.checked, view.subject)
tagged :: subject_type.kind == .record
discriminant := subject
if (view.optional_capture) discriminant = try optional_present(state, subject, subject_type, span)
if (tagged) {
tag_type :: typecheck.record_type(state.checked, subject_type.record_type).tag_type
discriminant = try emit(state, ir.Instruction{
op = .union_tag, type = lower_type(state, tag_type), left = subject, span = span,
})
}
type :: typecheck.type_of(state.checked, id)
runtime :: lower_type(state, type)
shape :: try array_shape(state, type)
result_slot := u32(0)
if (runtime != .void) result_slot = u32(try ir.add_local(state.module, ir.FunctionId(state.function), runtime, shape))
join :: try add_block(state)
falls := false
count :: ast.match_arm_count(tree, id)
for 0..count |i| {
arm :: ast.match_arm(tree, id, i)
arm_view :: ast.match_arm_view(tree, arm)
body :: try add_block(state)
if (arm_view.capture) |capture| {
capture_type :: typecheck.type_of(state.checked, capture)
local :: try ir.add_local(state.module, ir.FunctionId(state.function), lower_type(state, capture_type), try array_shape(state, capture_type))
state.storage[usize(capture)] = Storage{ kind = .local, index = u32(local) }
}
patterns :: ast.match_pattern_count(tree, arm)
if (arm_view.catch_all) {
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = body, span = span,
}
} else {
for 0..patterns |j| {
pattern :: ast.match_pattern(tree, arm, j)
selected :: try add_block(state)
final :: i + 1 == count and j + 1 == patterns
next := join
if (!final) {
condition :: try match_condition(state, discriminant, pattern)
if (state.terminated) return ir.no_value
next = try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = condition, target = selected, alternative = next, span = ast.node_span(tree, pattern),
}
} else {
# checking proves exhaustiveness; the final pattern is the remaining case.
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = selected, span = span,
}
}
state.block = selected
if (arm_view.capture) |capture| {
capture_type :: typecheck.type_of(state.checked, capture)
captured := subject
if (view.optional_capture) captured = try optional_payload(state, subject, subject_type, span)
if (tagged) captured = try emit(state, ir.Instruction{
op = .field, type = lower_type(state, capture_type), array_type = try array_shape(state, capture_type),
left = subject, slot = u32(typecheck.union_variant_index(state.checked, pattern)), span = ast.node_span(tree, pattern),
})
_ = try emit(state, ir.Instruction{
op = .store_local, left = captured, slot = state.storage[usize(capture)].index, span = span,
})
}
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = body, span = span,
}
state.block = next
}
}
next :: state.block
state.block = body
state.terminated = false
if (arm_view.expression) {
value :: try lower_as(state, arm_view.body, type)
if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{
op = .store_local, left = value, slot = result_slot, span = span,
})
} else {
reached :: try lower_value_body(state, arm_view.body, arm_view.body, type, result_slot, join)
if (reached) falls = true
}
if (!state.terminated) {
falls = true
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
}
state.block = next
state.terminated = false
}
state.block = join
state.terminated = !falls
if (!falls) state.module.functions.items[state.function].blocks.items[usize(join)].terminator = ir.Terminator{ kind = .unreachable }
if (!falls or runtime == .void) return ir.no_value
return try emit(state, ir.Instruction{
op = .load_local, type = runtime, array_type = shape, slot = result_slot, span = span,
})
}
@hide:file
lower_union proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
if (trap_type(state, id)) return ir.no_value
type :: typecheck.type_of(state.checked, id)
slot :: typecheck.union_variant_index(state.checked, id)
shape :: try array_shape(state, type)
field :: state.module.arrays.items[usize(shape)].fields[slot]
payload := ir.no_value
if (ast.expr_kind(tree, id) == .record_literal) {
view :: ast.record_literal(tree, id)
count :: ast.record_field_count(tree, id)
contextual := false
if (view.type) |annotation| contextual = ast.expr_kind(tree, annotation) == .enum_literal
if (contextual and field.type == .record and (count != 1 or ast.field(tree, ast.record_field(tree, id, 0)).name != null)) payload = try lower_record_as(state, id, typecheck.union_payload_type(state.checked, id))
else if (count != 0) payload = try lower_as(state, ast.field(tree, ast.record_field(tree, id, 0)).value?, typecheck.union_payload_type(state.checked, id))
}
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = u32(slot),
left = payload, constant = field.tag, span = ast.node_span(tree, id),
})
}
@hide:file
lower_record_as proc(state @mut State, id ast.NodeId, type typecheck.Type) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
explicit_count :: ast.record_field_count(tree, id)
if (type.kind != .record) {
for (0..explicit_count) |i| {
_ = try lower_expression(state, ast.field(tree, ast.record_field(tree, id, i)).value?)
if (state.terminated) return ir.no_value
}
_ = trap_type(state, id)
return ir.no_value
}
descriptor :: typecheck.record_type(state.checked, type.record_type)
try arraylist.reserve(&state.arguments, scratch_start + explicit_count)
for (0..explicit_count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..explicit_count) |i| {
initializer :: ast.field(tree, ast.record_field(tree, id, i)).value?
expected := typecheck.Type{}
for (descriptor.fields) |field, slot| {
if (typecheck.initializer_field(state.checked, id, slot) == initializer) {
expected = field.type
break
}
}
value :: try lower_as(state, initializer, expected)
if (state.terminated) return ir.no_value
state.arguments.items[scratch_start + i] = value
}
if (trap_type(state, id)) return ir.no_value
ordered_start :: state.arguments.items.len
try arraylist.reserve(&state.arguments, ordered_start + descriptor.fields.len)
for (descriptor.fields) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..(descriptor.fields.len)) |i| {
initializer :: typecheck.initializer_field(state.checked, id, i)
value := ir.no_value
# ponytail: quadratic field reorder; retain an inverse map if large records make lowering costly.
for (0..explicit_count) |j| {
if (ast.field(tree, ast.record_field(tree, id, j)).value? == initializer) {
value = state.arguments.items[scratch_start + j]
break
}
}
if (value == ir.no_value) value = try lower_as(state, initializer, descriptor.fields[i].type)
if (state.terminated) return ir.no_value
state.arguments.items[ordered_start + i] = value
}
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.reserve(&block.arguments, start + descriptor.fields.len)
for (0..(descriptor.fields.len)) |i| try arraylist.append(&block.arguments, state.arguments.items[ordered_start + i])
return try emit(state, ir.Instruction{
op = .record_literal,
type = .record,
array_type = try array_shape(state, type),
arguments_start = start,
arguments_count = descriptor.fields.len,
span = ast.node_span(tree, id),
})
}
@hide:file
is_place proc(state @State, id ast.NodeId) bool {
match ast.expr_kind(state.tree, id) {
.unary: {
expression :: ast.unary_expr(state.tree, id)
return expression.operator == .dereference
}
.index: {
base :: ast.index_expr(state.tree, id).base
kind :: typecheck.type_of(state.checked, base).kind
return kind == .pointer or kind == .slice or kind == .string or is_place(state, base)
}
.selection: {
if (sema.binding(state.resolution, id).kind == .none) {
base :: ast.selection_expr(state.tree, id).base
checked :: typecheck.type_of(state.checked, base)
if (checked.kind == .pointer) {
pointer :: typecheck.pointer_type(state.checked, checked.pointer_type)
return pointer.element.kind == .record
}
return is_place(state, base)
}
binding :: sema.binding(state.resolution, id)
if (binding.kind != .declaration) return false
location :: state.storage[usize(binding.data.declaration)]
return location.kind == .local or location.kind == .global
}
.identifier: {
binding :: sema.binding(state.resolution, id)
if (binding.kind == .invalid) return true
if (binding.kind != .declaration) return false
location :: state.storage[usize(binding.data.declaration)]
return location.kind == .local or location.kind == .global
}
else: return false
}
}
@hide:file
pointer_address proc(
state @mut State, pointer ir.ValueId, element typecheck.Type, span diagnostics.Span,
) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
return try emit(state, ir.Instruction{
op = .pointer_address, type = .address, left = pointer,
pointee = lower_type(state, element), array_type = try array_shape(state, element), span = span,
})
}
@hide:file
usize_constant proc(state @mut State, value usize, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
return try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = u64(value) } }, span)
}
@hide:file
null_constant proc(state @mut State, type ir.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
return try constant(state, ir.Value{ type = type, data = ir.ValueData{ unsigned = maxval!(u64) } }, span)
}
@hide:file
record_pair proc(state @mut State, shape ir.ArrayId, first, second ir.ValueId, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
block :: &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.append(&block.arguments, first)
try arraylist.append(&block.arguments, second)
return try emit(state, ir.Instruction{
op = .record_literal, type = .record, array_type = shape,
arguments_start = start, arguments_count = 2, span = span,
})
}
@hide:file
project proc(state @mut State, value ir.ValueId, type ir.Type, shape ir.ArrayId, slot u32, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
return try emit(state, ir.Instruction{ op = .field, type = type, array_type = shape, left = value, slot = slot, span = span })
}
@hide:file
optional_present proc(state @mut State, value ir.ValueId, type typecheck.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
child :: typecheck.array_type(state.checked, type.array_type).element
runtime :: lower_type(state, child)
if (child.kind == .pointer or child.kind == .function) {
absent :: try null_constant(state, runtime, span)
return try emit(state, ir.Instruction{ op = .not_equal, type = .bool, left = value, right = absent, span = span })
}
return try project(state, value, .bool, ir.no_array, 0, span)
}
@hide:file
optional_payload proc(state @mut State, value ir.ValueId, type typecheck.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
child :: typecheck.array_type(state.checked, type.array_type).element
runtime :: lower_type(state, child)
if (child.kind == .pointer or child.kind == .function) return value
return try project(state, value, runtime, try array_shape(state, child), 1, span)
}
@hide:file
zero_value proc(state @mut State, type ir.Type, shape ir.ArrayId, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
if (!ir.aggregate(type)) {
value := ir.Value{ type = type, data = ir.ValueData{ unsigned = 0 } }
if (type == .pointer or type == .function) value.data = ir.ValueData{ unsigned = maxval!(u64) }
return try constant(state, value, span)
}
descriptor :: state.module.arrays.items[usize(shape)]
if (descriptor.is_union) return try emit(state, ir.Instruction{ op = .union_literal, type = type, array_type = shape, span = span })
scratch :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch]
count :: if (type == .array) descriptor.len else descriptor.fields.len
for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..count) |i| {
child :: if (type == .array) descriptor.element else descriptor.fields[i].type
child_shape :: if (type == .array) descriptor.element_array else descriptor.fields[i].array_type
value :: try zero_value(state, child, child_shape, span)
state.arguments.items[scratch + i] = value
}
block :: &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch + i])
op ir.Op :: if (type == .array) .array_literal else .record_literal
return try emit(state, ir.Instruction{
op = op, type = type, array_type = shape,
arguments_start = start, arguments_count = count, span = span,
})
}
@hide:file
lower_as proc(state @mut State, id ast.NodeId, expected typecheck.Type) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
actual :: typecheck.type_of(state.checked, id)
span :: ast.node_span(state.tree, id)
if (actual.kind == .invalid or expected.kind == .invalid) return try lower_expression(state, id)
if (expected.kind == .fallible and actual.kind != .fallible) {
descriptor :: typecheck.array_type(state.checked, expected.array_type)
failed :: !typecheck.value_assignable(state.checked, actual, descriptor.element)
channel :: if (failed) descriptor.error_type else descriptor.element
payload :: try lower_as(state, id, channel)
if (state.terminated) return ir.no_value
return try wrap_fallible(state, payload, expected, failed, span)
}
if (expected.kind == .optional and actual.kind != .optional) {
child :: typecheck.array_type(state.checked, expected.array_type).element
runtime :: lower_type(state, child)
nullable :: child.kind == .pointer or child.kind == .function
if (actual.kind == .null and nullable) return try null_constant(state, runtime, span)
payload := ir.no_value
if (actual.kind == .null) payload = try zero_value(state, runtime, try array_shape(state, child), span)
else payload = try lower_as(state, id, child)
if (state.terminated or nullable) return payload
present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = actual.kind != .null } }, span)
return try record_pair(state, try array_shape(state, expected), present, payload, span)
}
if (expected.kind == .slice and actual.kind != .slice) {
parts :: try slice_parts(state, id)
if (state.terminated) return ir.no_value
return try record_pair(state, try array_shape(state, expected), parts.pointer, parts.bound, span)
}
value :: try lower_expression(state, id)
if (state.terminated) return ir.no_value
return try convert_value(state, value, actual, expected, span)
}
@hide:file
convert_value proc(state @mut State, value ir.ValueId, actual, expected typecheck.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
if (actual.kind == .fallible and expected.kind == .fallible) {
if (actual.array_type == expected.array_type) return value
source :: typecheck.array_type(state.checked, actual.array_type)
destination :: typecheck.array_type(state.checked, expected.array_type)
shape :: try array_shape(state, expected)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .record, shape))
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
for (0..2) |i| {
state.block = if (i == 0) success else failure
child :: if (i == 0) source.element else source.error_type
target :: if (i == 0) destination.element else destination.error_type
payload := ir.no_value
if (child.kind != .void) payload = try project(state, value, lower_type(state, child), try array_shape(state, child), u32(i), span)
converted :: try convert_value(state, payload, child, target, span)
wrapped :: try wrap_fallible(state, converted, expected, i == 1, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = wrapped, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
}
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .record, array_type = shape, slot = slot, span = span })
}
if (expected.kind == .optional and actual.kind != .optional) {
child :: typecheck.array_type(state.checked, expected.array_type).element
runtime :: lower_type(state, child)
nullable :: child.kind == .pointer or child.kind == .function
if (actual.kind == .null and nullable) return try null_constant(state, runtime, span)
payload := ir.no_value
if (actual.kind == .null) payload = try zero_value(state, runtime, try array_shape(state, child), span)
else payload = try convert_value(state, value, actual, child, span)
if (nullable) return payload
present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = actual.kind != .null } }, span)
return try record_pair(state, try array_shape(state, expected), present, payload, span)
}
if (actual.kind == .optional and expected.kind == .optional and actual.array_type != expected.array_type) {
source :: typecheck.array_type(state.checked, actual.array_type).element
child :: typecheck.array_type(state.checked, expected.array_type).element
if (source.kind == .pointer or source.kind == .function) return value
shape :: try array_shape(state, expected)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .record, shape))
present :: try optional_present(state, value, actual, span)
some :: try add_block(state)
none :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = present, target = some, alternative = none, span = span,
}
state.block = some
payload :: try optional_payload(state, value, actual, span)
converted :: try convert_value(state, payload, source, child, span)
wrapped :: try record_pair(state, shape, present, converted, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = wrapped, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = none
absent :: try zero_value(state, .record, shape, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = absent, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .record, array_type = shape, slot = slot, span = span })
}
if (expected.kind == .slice and actual.kind == .string) {
pointer :: try emit(state, ir.Instruction{ op = .string_pointer, type = .pointer, left = value, span = span })
length :: try emit(state, ir.Instruction{ op = .string_length, type = .usize, left = value, span = span })
return try record_pair(state, try array_shape(state, expected), pointer, length, span)
}
if (expected.kind == .slice and actual.kind == .array) {
source_shape :: try array_shape(state, actual)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .array, source_shape))
_ = try emit(state, ir.Instruction{ op = .store_local, left = value, slot = slot, span = span })
address :: try emit(state, ir.Instruction{
op = .address_local, type = .address, pointee = .array, array_type = source_shape, slot = slot, span = span,
})
pointer :: try emit(state, ir.Instruction{
op = .pointer_from_address, type = .pointer, left = address, pointee = .array, array_type = source_shape, span = span,
})
length :: try usize_constant(state, typecheck.array_type(state.checked, actual.array_type).len, span)
return try record_pair(state, try array_shape(state, expected), pointer, length, span)
}
if (
!typecheck.error_assignable(state.checked, actual, expected)
and !typecheck.error_assignable(state.checked, expected, actual)
) return value
source_union := false
if (actual.kind == .record) source_union = typecheck.record_type(state.checked, actual.record_type).is_union
if (expected.kind == .enumeration) {
if (!source_union) return value
return try emit(state, ir.Instruction{ op = .union_tag, type = lower_type(state, expected), left = value, span = span })
}
if (expected.kind != .record) return value
destination :: typecheck.record_type(state.checked, expected.record_type)
if (!destination.is_union) return value
if (actual.kind == .record and actual.record_type == expected.record_type) return value
shape :: try array_shape(state, expected)
if (actual.kind == .record and !source_union) {
for destination.fields |field, i| {
if (field.type.kind != .record or field.type.record_type != actual.record_type or field.type.nominal != actual.nominal) continue
return try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = u32(i), left = value,
constant = state.module.arrays.items[usize(shape)].fields[i].tag, span = span,
})
}
unreachable
}
discriminant := value
source_shape := ir.no_array
if (source_union) {
source_shape = try array_shape(state, actual)
tag_type :: typecheck.record_type(state.checked, actual.record_type).tag_type
discriminant = try emit(state, ir.Instruction{ op = .union_tag, type = lower_type(state, tag_type), left = value, span = span })
}
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .record, shape))
join :: try add_block(state)
for destination.fields |field, i| {
selected :: try add_block(state)
next :: try add_block(state)
tag :: state.module.arrays.items[usize(shape)].fields[i].tag
expected_tag :: try constant(state, tag, span)
condition :: try emit(state, ir.Instruction{ op = .equal, type = .bool, left = discriminant, right = expected_tag, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = condition, target = selected, alternative = next, span = span,
}
state.block = selected
payload := ir.no_value
if (source_union and field.type.kind != .void) {
source_fields :: state.module.arrays.items[usize(source_shape)].fields
for source_fields |source_field, j| {
if (source_field.tag.data.unsigned != tag.data.unsigned) continue
payload = try project(state, value, source_field.type, source_field.array_type, u32(j), span)
break
}
}
wrapped :: try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = u32(i), left = payload, constant = tag, span = span,
})
_ = try emit(state, ir.Instruction{ op = .store_local, left = wrapped, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = next
}
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ kind = .unreachable, span = span }
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .record, array_type = shape, slot = slot, span = span })
}
@hide:file
lower_try proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
operand :: ast.type_operand(state.tree, id)
value :: try lower_expression(state, operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
span :: ast.node_span(state.tree, id)
descriptor :: typecheck.array_type(state.checked, typecheck.type_of(state.checked, operand).array_type)
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
state.block = failure
payload :: try project(state, value, lower_type(state, descriptor.error_type), try array_shape(state, descriptor.error_type), 1, span)
target_error :: typecheck.array_type(state.checked, state.return_type.array_type).error_type
converted :: try convert_value(state, payload, descriptor.error_type, target_error, span)
result :: try wrap_fallible(state, converted, state.return_type, true, span)
try lower_cleanups(state, 0, converted)
if (!state.terminated) finish(state, result, span)
state.block = success
state.terminated = false
if (descriptor.element.kind == .void) return ir.no_value
return try project(state, value, lower_type(state, descriptor.element), try array_shape(state, descriptor.element), 0, span)
}
@hide:file
lower_catch proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.catch_expr(state.tree, id)
value :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
span :: expression.span
descriptor :: typecheck.array_type(state.checked, typecheck.type_of(state.checked, expression.operand).array_type)
type :: typecheck.type_of(state.checked, id)
runtime :: lower_type(state, type)
shape :: try array_shape(state, type)
slot := u32(0)
if (runtime != .void) slot = u32(try ir.add_local(state.module, ir.FunctionId(state.function), runtime, shape))
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
state.block = success
if (runtime != .void) {
payload :: try project(state, value, lower_type(state, descriptor.element), try array_shape(state, descriptor.element), 0, span)
converted :: try convert_value(state, payload, descriptor.element, type, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = converted, slot = slot, span = span })
}
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = failure
if (expression.capture) |capture| {
try predeclare_capture(state, capture)
payload :: try project(state, value, lower_type(state, descriptor.error_type), try array_shape(state, descriptor.error_type), 1, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = payload, slot = state.storage[usize(capture)].index, span = span })
}
if (!trap_type(state, expression.handler)) {
if (expression.handler_is_block) {
_ = try lower_value_body(state, expression.handler, expression.handler, type, slot, join)
} else {
fallback :: try lower_as(state, expression.handler, type)
if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{
op = .store_local, left = fallback, slot = slot, span = span,
})
}
}
if (!state.terminated) state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = join
state.terminated = false
if (runtime == .void) return ir.no_value
return try emit(state, ir.Instruction{ op = .load_local, type = runtime, array_type = shape, slot = slot, span = span })
}
@hide:file
SliceParts :: struct {
pointer ir.ValueId
bound ir.ValueId
element typecheck.Type
}
@hide:file
borrow_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
if (is_place(state, id)) return try lower_place(state, id)
value :: try lower_expression(state, id)
if (state.terminated) return ir.no_value
checked :: typecheck.type_of(state.checked, id)
type :: lower_type(state, checked)
shape :: try array_shape(state, checked)
span :: ast.node_span(state.tree, id)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), type, shape))
_ = try emit(state, ir.Instruction{ op = .store_local, type = type, array_type = shape, left = value, slot = slot, span = span })
return try emit(state, ir.Instruction{ op = .address_local, type = .address, pointee = type, array_type = shape, slot = slot, span = span })
}
@hide:file
slice_parts proc(state @mut State, id ast.NodeId) SliceParts ! (mem.AllocError | diagnostics.CapacityError) {
checked :: typecheck.type_of(state.checked, id)
span :: ast.node_span(state.tree, id)
result := SliceParts{ pointer = ir.no_value, bound = ir.no_value, element = typecheck.Type{} }
if (checked.kind == .slice) {
value :: try lower_expression(state, id)
if (state.terminated) return result
result.pointer = try project(state, value, .pointer, ir.no_array, 0, span)
result.bound = try project(state, value, .usize, ir.no_array, 1, span)
result.element = typecheck.array_type(state.checked, checked.array_type).element
return result
}
if (checked.kind == .string) {
value :: try lower_expression(state, id)
if (state.terminated) return result
result.pointer = try emit(state, ir.Instruction{ op = .string_pointer, type = .pointer, left = value, span = span })
result.bound = try emit(state, ir.Instruction{ op = .string_length, type = .usize, left = value, span = span })
result.element = typecheck.Type{ kind = .u8 }
return result
}
if (checked.kind == .pointer) {
descriptor :: typecheck.pointer_type(state.checked, checked.pointer_type)
result.pointer = try lower_expression(state, id)
if (state.terminated) return result
result.element = descriptor.element
if (!descriptor.many and descriptor.element.kind == .array) {
array :: typecheck.array_type(state.checked, descriptor.element.array_type)
result.element = array.element
result.bound = try usize_constant(state, array.len, span)
}
return result
}
if (checked.kind == .array) {
descriptor :: typecheck.array_type(state.checked, checked.array_type)
address :: try borrow_expression(state, id)
if (state.terminated) return result
result.pointer = try emit(state, ir.Instruction{
op = .pointer_from_address, type = .pointer, left = address,
pointee = .array, array_type = try array_shape(state, checked), span = span,
})
result.bound = try usize_constant(state, descriptor.len, span)
result.element = descriptor.element
return result
}
_ = try lower_expression(state, id)
return result
}
@hide:file
lower_slice proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.slice_expr(state.tree, id)
span :: ast.node_span(state.tree, id)
parts :: try slice_parts(state, expression.base)
if (state.terminated) return ir.no_value
start := ir.no_value
if (expression.start) |node| start = try lower_expression(state, node)
else start = try usize_constant(state, 0, span)
if (state.terminated) return ir.no_value
end := parts.bound
if (expression.end) |node| end = try lower_expression(state, node)
if (state.terminated or trap_type(state, id)) return ir.no_value
_ = try emit(state, ir.Instruction{ op = .slice_check, left = start, right = end, extra = parts.bound, span = span })
child :: parts.element
runtime :: lower_type(state, child)
shape :: try array_shape(state, child)
if (expression.sentinel) |node| {
sentinel :: try lower_as(state, node, child)
if (state.terminated) return ir.no_value
base_type :: typecheck.type_of(state.checked, expression.base)
has_sentinel := base_type.kind == .string
if (base_type.kind == .slice) has_sentinel = typecheck.array_type(state.checked, base_type.array_type).sentinel != null
if (parts.bound != ir.no_value and !has_sentinel) {
one :: try usize_constant(state, 1, span)
through :: try emit(state, ir.Instruction{ op = .add, type = .usize, left = end, right = one, span = span })
_ = try emit(state, ir.Instruction{ op = .slice_check, left = end, right = through, extra = parts.bound, span = span })
}
pointer :: try emit(state, ir.Instruction{ op = .pointer_offset, type = .pointer, left = parts.pointer, right = end, pointee = runtime, array_type = shape, span = span })
_ = try emit(state, ir.Instruction{ op = .sentinel_check, left = pointer, right = sentinel, pointee = runtime, array_type = shape, span = span })
}
pointer :: try emit(state, ir.Instruction{ op = .pointer_offset, type = .pointer, left = parts.pointer, right = start, pointee = runtime, array_type = shape, span = span })
length :: try emit(state, ir.Instruction{ op = .subtract, type = .usize, left = end, right = start, span = span })
return try record_pair(state, try array_shape(state, typecheck.type_of(state.checked, id)), pointer, length, span)
}
@hide:file
lower_builtin proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
span :: ast.node_span(tree, id)
name :: ast.literal_text(tree, ast.builtin_call(tree, id).name)
if (trap_type(state, id)) return ir.no_value
if (mem.eql(name, "some")) {
type :: typecheck.type_of(state.checked, id)
child :: typecheck.array_type(state.checked, type.array_type).element
payload :: try lower_as(state, ast.call_arg(tree, id, 0), child)
if (state.terminated or child.kind == .pointer or child.kind == .function) return payload
present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = true } }, span)
return try record_pair(state, try array_shape(state, type), present, payload, span)
}
if (mem.eql(name, "maxval") or mem.eql(name, "minval")) {
type :: lower_type(state, typecheck.type_of(state.checked, id))
value := ir.Value{ type = type }
if (mem.eql(name, "maxval")) value.data = ir.ValueData{ unsigned = enummap.get(&ir.integer_maxima, type)? }
else value.data = ir.ValueData{ integer = enummap.get(&ir.integer_minima, type)? }
return try constant(state, value, span)
}
if (mem.eql(name, "divtrunc")) {
left :: try lower_expression(state, ast.call_arg(tree, id, 0))
if (state.terminated) return ir.no_value
right :: try lower_expression(state, ast.call_arg(tree, id, 1))
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .divide, type = lower_type(state, typecheck.type_of(state.checked, id)),
left = left, right = right, span = span,
})
}
if (mem.eql(name, "sizeof") or mem.eql(name, "alignof")) {
checked :: typecheck.type_of(state.checked, ast.call_arg(tree, id, 0))
type :: lower_type(state, checked)
shape :: try array_shape(state, checked)
size :: if (mem.eql(name, "sizeof")) ir.storage_size(state.module, type, shape) else ir.storage_alignment(state.module, type, shape)
return try usize_constant(state, size, span)
}
if (mem.eql(name, "ptrcast") or mem.eql(name, "constcast")) {
operand :: ast.call_arg(tree, id, ast.call_arg_count(tree, id) - 1)
value :: try lower_expression(state, operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
type :: typecheck.type_of(state.checked, id)
if (lower_type(state, type) == .record) return value
return try emit(state, ir.Instruction{ op = .pointer_cast, type = lower_type(state, type), left = value, span = span })
}
if (mem.eql(name, "memcopy") or mem.eql(name, "memset")) {
destination :: try slice_parts(state, ast.call_arg(tree, id, 0))
if (state.terminated) return ir.no_value
source := SliceParts{ pointer = ir.no_value, bound = ir.no_value, element = typecheck.Type{} }
value := ir.no_value
if (mem.eql(name, "memcopy")) source = try slice_parts(state, ast.call_arg(tree, id, 1))
else value = try lower_as(state, ast.call_arg(tree, id, 1), destination.element)
if (state.terminated or trap_type(state, id)) return ir.no_value
if (mem.eql(name, "memcopy")) {
zero :: try usize_constant(state, 0, span)
_ = try emit(state, ir.Instruction{ op = .slice_check, left = zero, right = source.bound, extra = destination.bound, span = span })
_ = try emit(state, ir.Instruction{ op = .slice_check, left = zero, right = destination.bound, extra = source.bound, span = span })
value = source.pointer
}
op ir.Op :: if (mem.eql(name, "memcopy")) .memory_copy else .memory_set
_ = try emit(state, ir.Instruction{
op = op,
left = destination.pointer, right = value, extra = destination.bound,
pointee = lower_type(state, destination.element), array_type = try array_shape(state, destination.element), span = span,
})
return ir.no_value
}
if (!trap_type(state, id)) trap(state, try diagnostics.add(state.diagnostics, .invalid_operand, span), span)
return ir.no_value
}
@hide:file
lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
checked :: typecheck.type_of(state.checked, id)
if (ast.expr_kind(state.tree, id) == .unary) {
expression :: ast.unary_expr(state.tree, id)
pointer :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try pointer_address(state, pointer, checked, span)
}
if (
ast.expr_kind(state.tree, id) == .selection
and sema.binding(state.resolution, id).kind == .none
) {
expression :: ast.selection_expr(state.tree, id)
base_type :: typecheck.type_of(state.checked, expression.base)
base := ir.no_value
if (base_type.kind == .pointer) {
pointer :: try lower_expression(state, expression.base)
if (state.terminated or trap_type(state, id)) return ir.no_value
element :: typecheck.pointer_type(state.checked, base_type.pointer_type).element
base = try pointer_address(state, pointer, element, span)
} else base = try lower_place(state, expression.base)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .field_address,
type = .address,
pointee = lower_type(state, checked),
array_type = try array_shape(state, checked),
left = base,
slot = u32(typecheck.field_index(state.checked, id)),
span = span,
})
}
if (ast.expr_kind(state.tree, id) == .index) {
expression :: ast.index_expr(state.tree, id)
base_type :: typecheck.type_of(state.checked, expression.base)
if (base_type.kind == .slice or base_type.kind == .string) {
parts :: try slice_parts(state, expression.base)
if (state.terminated) return ir.no_value
index :: try lower_expression(state, expression.index)
if (state.terminated or trap_type(state, id)) return ir.no_value
one :: try usize_constant(state, 1, span)
end :: try emit(state, ir.Instruction{ op = .add, type = .usize, left = index, right = one, span = span })
_ = try emit(state, ir.Instruction{ op = .slice_check, left = index, right = end, extra = parts.bound, span = span })
pointer :: try emit(state, ir.Instruction{
op = .pointer_offset, type = .pointer, left = parts.pointer, right = index,
pointee = lower_type(state, checked), array_type = try array_shape(state, checked), span = span,
})
return try pointer_address(state, pointer, checked, span)
}
base := ir.no_value
if (base_type.kind == .pointer) {
pointer :: try lower_expression(state, expression.base)
if (state.terminated or trap_type(state, id)) return ir.no_value
descriptor :: typecheck.pointer_type(state.checked, base_type.pointer_type)
if (descriptor.many) {
index :: try lower_expression(state, expression.index)
if (state.terminated) return ir.no_value
offset :: try emit(state, ir.Instruction{
op = .pointer_offset, type = .pointer, left = pointer, right = index,
pointee = lower_type(state, checked), array_type = try array_shape(state, checked), span = span,
})
return try pointer_address(state, offset, checked, span)
}
base = try pointer_address(state, pointer, descriptor.element, span)
} else base = try lower_place(state, expression.base)
if (state.terminated) return ir.no_value
index :: try lower_expression(state, expression.index)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .index_address,
type = .address,
pointee = lower_type(state, checked),
array_type = try array_shape(state, checked),
left = base,
right = index,
span = span,
})
}
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
binding :: sema.binding(state.resolution, id)
location :: state.storage[usize(binding.data.declaration)]
operation ir.Op :: if (location.kind == .global) .address_global else .address_local
return try emit(state, ir.Instruction{
op = operation,
type = .address,
pointee = lower_type(state, checked),
array_type = try array_shape(state, checked),
slot = location.index,
span = span,
})
}
@hide:file
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
span :: ast.node_span(tree, id)
type :: lower_type(state, typecheck.type_of(state.checked, id))
shape :: try array_shape(state, typecheck.type_of(state.checked, id))
if (typecheck.is_enum_member(state.checked, id)) {
if (trap_type(state, id)) return ir.no_value
return try constant(state, enum_value(state, typecheck.type_of(state.checked, id), typecheck.field_index(state.checked, id)), span)
}
if (typecheck.is_union_variant(state.checked, id)) return try lower_union(state, id)
match ast.expr_kind(tree, id) {
.comptime_expr: {
if (!trap_type(state, id)) unreachable
}
.typed_constant: {
if (trap_type(state, id)) return ir.no_value
return try lower_as(state, ast.typed_constant(tree, id).value, typecheck.type_of(state.checked, id))
}
.union_constant: {
if (trap_type(state, id)) return ir.no_value
bytes :: ast.union_constant(tree, id).value
text :: ast.literal_text(tree, bytes)
count :: ir.storage_size(state.module, .record, shape)
byte_shape :: try ir.add_array(state.module, .u8, ir.no_array, count, false)
scratch :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch]
cursor := usize(1)
while (cursor < text.len - 1) {
byte :: ast.string_next(text, &cursor) catch |_| {
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
return ir.no_value
}
element :: try constant(state, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = u64(byte) } }, span)
try arraylist.append(&state.arguments, element)
}
debug.assert(state.arguments.items.len - scratch == count)
block :: &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
for (state.arguments.items[scratch..]) |element| try arraylist.append(&block.arguments, element)
payload :: try emit(state, ir.Instruction{
op = .array_literal, type = .array, array_type = byte_shape,
arguments_start = start, arguments_count = count, span = span,
})
return try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, left = payload, span = span,
})
}
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
}
.range: {
# ranges have no physical value; only lower_for decomposes an iterable.
if (!trap_type(state, id)) unreachable
}
.array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type, .union_tag_type, .slice_type, .optional_type, .fallible_type, .sum_type: {
if (!trap_type(state, id)) trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span)
}
.alias_expr: return try lower_expression(state, ast.type_operand(tree, id))
.literal_undefined: {
if (trap_type(state, id)) return ir.no_value
checked :: typecheck.type_of(state.checked, id)
return try emit(state, ir.Instruction{
op = .undefined_value, type = lower_type(state, checked),
array_type = try array_shape(state, checked), span = span,
})
}
.enum_literal: {
if (!trap_type(state, id)) unreachable
}
.block_expr: {
if (trap_type(state, id)) return ir.no_value
return try lower_value_block(state, ast.block_expr(tree, id), id, typecheck.type_of(state.checked, id))
}
.match_expr: return try lower_match(state, id)
.try_expr: return try lower_try(state, id)
.catch_expr: return try lower_catch(state, id)
.literal_null: return try null_constant(state, .pointer, span)
.literal_void: return ir.no_value
.slice: return try lower_slice(state, id)
.builtin_call: return try lower_builtin(state, id)
.record_literal: {
checked :: typecheck.type_of(state.checked, id)
if (checked.kind == .record and typecheck.record_type(state.checked, checked.record_type).is_union) return try lower_union(state, id)
return try lower_record_as(state, id, checked)
}
.array_literal: {
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
count :: ast.array_element_count(tree, id)
try arraylist.reserve(&state.arguments, scratch_start + count)
for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..count) |i| {
expected :: typecheck.array_type(state.checked, typecheck.type_of(state.checked, id).array_type).element
element :: try lower_as(state, ast.array_element(tree, id, i), expected)
if (state.terminated) return ir.no_value
state.arguments.items[scratch_start + i] = element
}
if (trap_type(state, id)) return ir.no_value
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.reserve(&block.arguments, start + count)
for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch_start + i])
return try emit(state, ir.Instruction{
op = .array_literal,
type = .array,
array_type = shape,
arguments_start = start,
arguments_count = count,
span = span,
})
}
.index: {
if (is_place(state, id)) {
address :: try lower_place(state, id)
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .load_address, type = type, array_type = shape, left = address, span = span,
})
}
expression :: ast.index_expr(tree, id)
base :: try lower_expression(state, expression.base)
if (state.terminated) return ir.no_value
index :: try lower_expression(state, expression.index)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .index, type = type, array_type = shape, left = base, right = index, span = span,
})
}
.literal_int, .literal_float: {
if (trap_type(state, id)) return ir.no_value
return try lower_number(state, id)
}
.literal_bool: {
if (trap_type(state, id)) return ir.no_value
return try constant(state, ir.Value{
type = .bool,
data = ir.ValueData{ boolean = mem.eql(ast.literal_text(tree, id), "true") },
}, span)
}
.literal_string: {
if (trap_type(state, id)) return ir.no_value
return try lower_string(state, id)
}
.identifier, .selection: {
if (
ast.expr_kind(tree, id) == .selection
and sema.binding(state.resolution, id).kind == .none
) {
selection :: ast.selection_expr(tree, id)
if (trap_type(state, id)) return ir.no_value
base_type :: typecheck.type_of(state.checked, selection.base)
if (base_type.kind == .slice or base_type.kind == .string or base_type.kind == .array) {
member :: ast.literal_text(tree, selection.member)
if (mem.eql(member, "len") or mem.eql(member, "ptr")) {
parts :: try slice_parts(state, selection.base)
if (state.terminated or trap_type(state, id)) return ir.no_value
return if (mem.eql(member, "len")) parts.bound else parts.pointer
}
}
if (base_type.kind == .pointer) {
descriptor :: typecheck.pointer_type(state.checked, base_type.pointer_type)
if (!descriptor.many and descriptor.element.kind == .array) {
pointer :: try lower_expression(state, selection.base)
if (state.terminated or trap_type(state, id)) return ir.no_value
member :: ast.literal_text(tree, selection.member)
array :: typecheck.array_type(state.checked, descriptor.element.array_type)
if (mem.eql(member, "len")) return try constant(state, ir.Value{
type = .usize, data = ir.ValueData{ unsigned = u64(array.len) },
}, span)
if (mem.eql(member, "ptr")) {
zero :: try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = 0 } }, span)
return try emit(state, ir.Instruction{
op = .pointer_offset, type = .pointer, left = pointer, right = zero,
pointee = lower_type(state, array.element), array_type = try array_shape(state, array.element), span = span,
})
}
}
}
if (is_place(state, id)) {
address :: try lower_place(state, id)
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .load_address, type = type, array_type = shape, left = address, span = span,
})
}
base :: try lower_expression(state, ast.selection_expr(tree, id).base)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .field, type = type, array_type = shape, left = base,
slot = u32(typecheck.field_index(state.checked, id)), span = span,
})
}
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
binding :: sema.binding(state.resolution, id)
location :: state.storage[usize(binding.data.declaration)]
match location.kind {
.function: return try constant(state, ir.Value{
type = .function,
data = ir.ValueData{ function = ir.FunctionId(location.index) },
}, span)
.global, .local: {
declaration_type :: typecheck.type_of(state.checked, binding.data.declaration)
operation :: if (location.kind == .global) ir.Op.load_global else ir.Op.load_local
value :: try emit(state, ir.Instruction{
op = operation, type = lower_type(state, declaration_type),
array_type = try array_shape(state, declaration_type), slot = location.index, span = span,
})
return try convert_value(state, value, declaration_type, typecheck.type_of(state.checked, id), span)
}
.none: unreachable
}
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .unwrap) {
operand :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
source :: typecheck.type_of(state.checked, expression.operand)
present :: try optional_present(state, operand, source, span)
_ = try emit(state, ir.Instruction{ op = .optional_check, left = present, span = span })
return try optional_payload(state, operand, source, span)
}
if (expression.operator == .address_of) {
if (typecheck.type_of(state.checked, id).kind == .slice) {
parts :: try slice_parts(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try record_pair(state, shape, parts.pointer, parts.bound, span)
}
if (!is_place(state, expression.operand)) {
_ = try lower_expression(state, expression.operand)
if (!state.terminated) _ = trap_type(state, id)
return ir.no_value
}
address :: try lower_place(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
element :: typecheck.type_of(state.checked, expression.operand)
return try emit(state, ir.Instruction{
op = .pointer_from_address, type = .pointer, left = address,
pointee = lower_type(state, element), array_type = try array_shape(state, element), span = span,
})
}
if (expression.operator == .dereference) {
address :: try lower_place(state, id)
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .load_address, type = type, array_type = shape, left = address, span = span,
})
}
if (expression.operator == .negate and ir.integer_bits(type) != 0 and !ir.unsigned_integer(type) and ast.expr_kind(tree, expression.operand) == .literal_int) {
text :: ast.literal_text(tree, expression.operand)
magnitude_limit :: enummap.get(&ir.integer_maxima, type)? + 1
if (integer_magnitude(text, magnitude_limit)) |magnitude| {
if (magnitude == magnitude_limit) return try constant(state, ir.Value{ type = type, data = ir.ValueData{ integer = enummap.get(&ir.integer_minima, type)? } }, span)
}
}
operand :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
op :: enummap.get(&unary_ops, expression.operator)?
return try emit(state, ir.Instruction{ op = op, type = type, left = operand, span = span })
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (expression.operator == .logical_and or expression.operator == .logical_or) return try lower_logical(state, id)
left_type :: typecheck.type_of(state.checked, expression.left)
right_type :: typecheck.type_of(state.checked, expression.right)
if (
(expression.operator == .equal or expression.operator == .not_equal)
and (left_type.kind == .null or right_type.kind == .null)
) {
optional_id :: if (left_type.kind == .null) expression.right else expression.left
value :: try lower_expression(state, optional_id)
if (state.terminated or trap_type(state, id)) return ir.no_value
present :: try optional_present(state, value, typecheck.type_of(state.checked, optional_id), span)
if (expression.operator == .not_equal) return present
return try emit(state, ir.Instruction{ op = .logical_not, type = .bool, left = present, span = span })
}
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
right :: try lower_expression(state, expression.right)
if (state.terminated or trap_type(state, id)) return ir.no_value
if (expression.operator == .add and type == .pointer) {
pointer :: typecheck.pointer_type(state.checked, typecheck.type_of(state.checked, id).pointer_type)
return try emit(state, ir.Instruction{
op = .pointer_offset, type = .pointer, left = left, right = right,
pointee = lower_type(state, pointer.element),
array_type = try array_shape(state, pointer.element), span = span,
})
}
op :: enummap.get(&binary_ops, expression.operator)?
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
.call: {
target :: sema.binding(state.resolution, ast.call_expr(tree, id).callee)
if (target.kind == .builtin or typecheck.is_type_constructor(state.checked, id)) {
operand := ir.no_value
for (0..ast.call_arg_count(tree, id)) |i| {
value :: try lower_expression(state, ast.call_arg(tree, id, i))
if (state.terminated) return ir.no_value
if (i == 0) operand = value
}
if (trap_type(state, id)) return ir.no_value
if (typecheck.is_type_constructor(state.checked, id)) {
source :: lower_type(state, typecheck.type_of(state.checked, ast.call_arg(tree, id, 0)))
if (source == type) return operand
}
return try emit(state, ir.Instruction{ op = .convert, type = type, left = operand, span = span })
}
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
if (state.terminated) return ir.no_value
# recursion borrows later scratch slots; no items pointer survives a child.
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
count :: ast.call_arg_count(tree, id)
try arraylist.reserve(&state.arguments, scratch_start + count)
for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..count) |i| {
signature :: typecheck.type_of(state.checked, ast.call_expr(tree, id).callee).function
expected := typecheck.Type{}
if (signature != ast.NodeId(maxval!(u32)) and i < ast.function_param_count(tree, signature)) expected = typecheck.type_of(state.checked, ast.param(tree, ast.function_param(tree, signature, i)).type)
argument :: try lower_as(state, ast.call_arg(tree, id, i), expected)
if (state.terminated) return ir.no_value
state.arguments.items[scratch_start + i] = argument
}
if (trap_type(state, id)) return ir.no_value
# branchful arguments end in a different block from the callee.
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.reserve(&block.arguments, start + count)
for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch_start + i])
return try emit(state, ir.Instruction{
op = .call,
type = type,
array_type = shape,
left = callee,
arguments_start = start,
arguments_count = count,
span = span,
})
}
}
return ir.no_value
}
@hide:file
integer_magnitude proc(text []u8, limit u64) ?u64 {
value := u64(0)
for (text) |byte| {
if (byte < '0' or byte > '9') return null
digit :: u64(byte - '0')
if (digit > limit or value > divtrunc!(limit - digit, u64(10))) return null
value = value * 10 + digit
}
return value
}
@hide:file
exact_float_integer proc(text []u8, value f64) bool {
# every finite f64 integer has at most 309 decimal digits, plus a terminator.
decimal [310]mut u8 := undefined
count :: c.snprintf(ptrcast!((&decimal).ptr), c_ulong(decimal.len), "%.0f", value)
if (count < 0 or usize(count) >= decimal.len) return false
start := usize(0)
while (start + 1 < text.len and text[start] == '0') start += 1
return mem.eql(text[start..], decimal[..usize(count)])
}
@hide:file
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
if (ast.float_value(state.tree, id)) |value| {
return try constant(state, ir.Value{
type = .float,
data = ir.ValueData{ real = value },
}, span)
}
text :: ast.literal_text(state.tree, id)
type :: lower_type(state, typecheck.type_of(state.checked, id))
if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) {
if (integer_magnitude(text, enummap.get(&ir.integer_maxima, type)?)) |magnitude| return try constant(state, ir.Value{ type = type, data = ir.ValueData{ integer = i64(magnitude) } }, span)
} else if (ir.unsigned_integer(type)) {
if (integer_magnitude(text, enummap.get(&ir.integer_maxima, type)?)) |magnitude| return try constant(state, ir.Value{ type = type, data = ir.ValueData{ unsigned = magnitude } }, span)
} else {
value :: strconv.parse_float(state.module.allocator, text) catch |err| {
match err {
.out_of_memory: return err
.invalid_number, .out_of_range: {
trap(state, try diagnostics.add(state.diagnostics, .literal_out_of_range, span), span)
return ir.no_value
}
}
}
if (ast.expr_kind(state.tree, id) == .literal_int and !exact_float_integer(text, value)) {
trap(state, try diagnostics.add(state.diagnostics, .inexact_integer_literal, span), span)
return ir.no_value
}
return try constant(state, ir.Value{ type = .float, data = ir.ValueData{ real = value } }, span)
}
trap(state, try diagnostics.add(state.diagnostics, .literal_out_of_range, span), span)
return ir.no_value
}
@hide:file
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
text :: ast.literal_text(state.tree, id)
owned :: try mem.alloc(u8, state.module.allocator, text.len - 2)
transferred := false
defer if (!transferred) mem.free(state.module.allocator, owned)
length := usize(0)
cursor := usize(1)
end :: text.len - 1
while (cursor < end) {
byte :: ast.string_next(text, &cursor) catch |_| {
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
return ir.no_value
}
owned[length] = byte
length += 1
}
if (state.module.strings.items.len > usize(maxval!(ir.StringId)))
return .out_of_memory
string_id :: ir.StringId(state.module.strings.items.len)
try arraylist.append(&state.module.strings, ir.StringStorage{
allocation = owned,
len = length,
})
transferred = true
return try constant(state, ir.Value{
type = .string,
data = ir.ValueData{ string = string_id },
}, span)
}