feat: support enums, distinct types, unions and matching

This commit is contained in:
2026-10-25 23:16:09 +01:00
parent 280059d83d
commit 9966a1f523
23 changed files with 3049 additions and 176 deletions
+189 -4
View File
@@ -1,12 +1,14 @@
#! grammar:
#! * type ..... -> identifier {'.' identifier} | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' [field {(',' | newline) field}] '}'
#! * type ..... -> identifier {'.' member} | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
#! * expr ..... -> identifier | literal | type | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' (identifier | int) | expr ('..' | '..=') expr
#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}'
#! * decl ..... -> identifier [type] binding expr | ['test'] [identifier '::'] 'import' string
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated.
#! enum members and tagged-union variants accept keyword names; member names do not enter lexical scopes.
#! * stmt ..... -> decl | expr | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
#! braceless guards/iterables require parentheses unless they are calls; else binds to the nearest if.
#! precedence (tightest first): calls/indices/selections, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses.
@@ -45,6 +47,12 @@ ExprKind :: enum {
array_literal
record_type
record_literal
enum_type
union_type
distinct_type
alias_expr
enum_literal
match_expr
index
selection
invalid
@@ -101,6 +109,13 @@ NodeKind :: enum {
expr_record_type
expr_record_literal
record_field
expr_enum_type
expr_union_type
expr_distinct_type
expr_alias
expr_enum_literal
expr_match
match_arm
expr_index
expr_selection
stmt_expr
@@ -228,6 +243,16 @@ FieldView :: struct {
value ?NodeId
}
EnumTypeView :: struct { backing ?NodeId }
UnionTypeView :: struct { tag ?NodeId, auto_tag bool }
MatchView :: struct { subject NodeId }
MatchArmView :: struct {
capture ?NodeId
body NodeId
expression bool
catch_all bool
}
IndexView :: struct {
span Span
base NodeId
@@ -477,6 +502,78 @@ add_record_type proc(builder @mut Builder, span Span, fields []NodeId) NodeId !
}, span)
}
add_enum_type proc(builder @mut Builder, span Span, backing ?NodeId, members []NodeId) NodeId ! mem.AllocError {
start :: builder.storage.extra.items.len
for (members) |member| _ = try add_extra(builder, u32(member))
raw_backing :: if (backing) |id| id else no_node
return try add_node(builder, Node{
kind = .expr_enum_type,
main = NodeData{ node_id = raw_backing },
data0 = NodeData{ extra = u32(start) },
data1 = NodeData{ extra = u32(builder.storage.extra.items.len) },
}, span)
}
add_union_type proc(builder @mut Builder, span Span, tag ?NodeId, auto_tag bool, fields []NodeId) NodeId ! mem.AllocError {
raw_tag :: if (tag) |id| u32(id) else u32(no_node)
tag_flag :: if (auto_tag) u32(1) else u32(0)
metadata :: try add_extra(builder, raw_tag)
_ = try add_extra(builder, tag_flag)
start :: builder.storage.extra.items.len
for (fields) |field_id| _ = try add_extra(builder, u32(field_id))
return try add_node(builder, Node{
kind = .expr_union_type,
main = NodeData{ extra = metadata },
data0 = NodeData{ extra = u32(start) },
data1 = NodeData{ extra = u32(builder.storage.extra.items.len) },
}, span)
}
add_type_operand proc(builder @mut Builder, span Span, operand NodeId, transparent bool) NodeId ! mem.AllocError {
kind :: if (transparent) NodeKind.expr_alias else NodeKind.expr_distinct_type
return try add_node(builder, Node{
kind = kind,
main = NodeData{ node_id = operand },
}, span)
}
add_enum_literal proc(builder @mut Builder, span Span, member NodeId) NodeId ! mem.AllocError {
return try add_node(builder, Node{
kind = .expr_enum_literal,
main = NodeData{ node_id = member },
}, span)
}
add_match proc(builder @mut Builder, span Span, subject NodeId, arms []NodeId) NodeId ! mem.AllocError {
start :: builder.storage.extra.items.len
for (arms) |arm| _ = try add_extra(builder, u32(arm))
return try add_node(builder, Node{
kind = .expr_match,
main = NodeData{ node_id = subject },
data0 = NodeData{ extra = u32(start) },
data1 = NodeData{ extra = u32(builder.storage.extra.items.len) },
}, span)
}
add_match_arm proc(
builder @mut Builder, span Span, patterns []NodeId, capture ?NodeId,
body NodeId, expression, catch_all bool,
) NodeId ! mem.AllocError {
raw_capture :: if (capture) |id| u32(id) else u32(no_node)
expression_flag :: if (expression) u32(1) else u32(0)
catch_all_flag :: if (catch_all) u32(1) else u32(0)
start :: try add_extra(builder, raw_capture)
_ = try add_extra(builder, expression_flag)
_ = try add_extra(builder, catch_all_flag)
for (patterns) |pattern| _ = try add_extra(builder, u32(pattern))
return try add_node(builder, Node{
kind = .match_arm,
main = NodeData{ node_id = body },
data0 = NodeData{ extra = start },
data1 = NodeData{ extra = u32(builder.storage.extra.items.len) },
}, span)
}
add_record_literal proc(
builder @mut Builder, span Span, type ?NodeId, fields []NodeId,
) NodeId ! mem.AllocError {
@@ -979,6 +1076,12 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_array_literal: .array_literal
.expr_record_type: .record_type
.expr_record_literal: .record_literal
.expr_enum_type: .enum_type
.expr_union_type: .union_type
.expr_distinct_type: .distinct_type
.expr_alias: .alias_expr
.expr_enum_literal: .enum_literal
.expr_match: .match_expr
.expr_index: .index
.expr_selection: .selection
.invalid_expr: .invalid
@@ -1084,15 +1187,97 @@ field proc(tree @Tree, id NodeId) FieldView {
}
}
enum_type proc(tree @Tree, id NodeId) EnumTypeView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_enum_type)
return EnumTypeView{ backing = maybe_node_id(u32(node.main.node_id)) }
}
enum_member_count proc(tree @Tree, id NodeId) usize {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_enum_type)
return usize(node.data1.extra - node.data0.extra)
}
enum_member proc(tree @Tree, id NodeId, index usize) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(index < enum_member_count(tree, id))
return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index])
}
union_type proc(tree @Tree, id NodeId) UnionTypeView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_union_type)
extra :: usize(node.main.extra)
return UnionTypeView{
tag = maybe_node_id(tree.storage.extra.items[extra]),
auto_tag = tree.storage.extra.items[extra + 1] != 0,
}
}
type_operand proc(tree @Tree, id NodeId) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias)
return node.main.node_id
}
enum_literal proc(tree @Tree, id NodeId) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_enum_literal)
return node.main.node_id
}
match_expr proc(tree @Tree, id NodeId) MatchView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_match)
return MatchView{ subject = node.main.node_id }
}
match_arm_count proc(tree @Tree, id NodeId) usize {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_match)
return usize(node.data1.extra - node.data0.extra)
}
match_arm proc(tree @Tree, id NodeId, index usize) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(index < match_arm_count(tree, id))
return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index])
}
match_arm_view proc(tree @Tree, id NodeId) MatchArmView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .match_arm)
extra :: usize(node.data0.extra)
return MatchArmView{
capture = maybe_node_id(tree.storage.extra.items[extra]),
body = node.main.node_id,
expression = tree.storage.extra.items[extra + 1] != 0,
catch_all = tree.storage.extra.items[extra + 2] != 0,
}
}
match_pattern_count proc(tree @Tree, id NodeId) usize {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .match_arm)
return usize(node.data1.extra - node.data0.extra - 3)
}
match_pattern proc(tree @Tree, id NodeId, index usize) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(index < match_pattern_count(tree, id))
return NodeId(tree.storage.extra.items[usize(node.data0.extra) + 3 + index])
}
record_field_count proc(tree @Tree, id NodeId) usize {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_record_type or node.kind == .expr_record_literal)
debug.assert(node.kind == .expr_record_type or node.kind == .expr_record_literal or node.kind == .expr_union_type)
return usize(node.data1.extra - node.data0.extra)
}
record_field proc(tree @Tree, id NodeId, index usize) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_record_type or node.kind == .expr_record_literal)
debug.assert(node.kind == .expr_record_type or node.kind == .expr_record_literal or node.kind == .expr_union_type)
debug.assert(index < usize(node.data1.extra - node.data0.extra))
return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index])
}
+26
View File
@@ -76,6 +76,19 @@ Code :: enum {
namespace_not_value
not_namespace
invalid_import_alias
invalid_enum_backing
invalid_enum_value
enum_context_required
invalid_distinct_backing
invalid_union_tag
union_initializer_count
invalid_match_subject
invalid_match_pattern
duplicate_match_pattern
non_exhaustive_match
unreachable_match_arm
invalid_match_capture
inactive_union_field
}
Diagnostic :: struct {
@@ -173,5 +186,18 @@ messages :: $enummap.init(Code, []u8, {
namespace_not_value = "package namespace cannot be used as a value",
not_namespace = "member selection requires a package namespace",
invalid_import_alias = "import requires an explicit valid identifier alias",
invalid_enum_backing = "enum backing must be a concrete integer type",
invalid_enum_value = "enum values require explicit backing and increasing representable constant integers",
enum_context_required = "enum literal requires an enum or tagged union context",
invalid_distinct_backing = "distinct requires a concrete runtime backing type",
invalid_union_tag = "union tag must be an enum containing every union variant",
union_initializer_count = "union construction requires exactly one member",
invalid_match_subject = "match subject must be an enum, tagged union, or scalar",
invalid_match_pattern = "invalid match pattern",
duplicate_match_pattern = "duplicate or overlapping match pattern",
non_exhaustive_match = "match is not exhaustive",
unreachable_match_arm = "match arm is unreachable",
invalid_match_capture = "payload capture requires compatible nonvoid tagged union variants",
inactive_union_field = "tagged union field is not active",
})
+50 -18
View File
@@ -41,6 +41,7 @@ Address :: struct {
cells []mut ir.Value
scalar ?@mut ir.Value = null
initialized ?@mut bool = null
raw bool = false
}
@hide:file
@@ -104,6 +105,7 @@ run proc(
if instruction_index == block.instructions.items.len {
terminator :: block.terminator
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
if (terminator.kind == .unreachable) unreachable
if (terminator.kind == .jump or terminator.kind == .branch) {
target := terminator.target
if (terminator.kind == .branch) {
@@ -121,7 +123,7 @@ run proc(
completed :: frames.items[frame_index]
# transfer leaves while the completed frame still owns its backing.
if completed.global |global_index| {
copy_value(&globals[global_index].value, value)
copy_value(module, &globals[global_index].value, value)
globals[global_index].status = .ready
}
if (frames.items.len == 1) {
@@ -134,7 +136,7 @@ run proc(
return Outcome{ value = value }
}
if completed.destination != ir.no_value {
copy_value(&frames.items[frame_index - 1].values[usize(completed.destination)], value)
copy_value(module, &frames.items[frame_index - 1].values[usize(completed.destination)], value)
}
_ = arraylist.pop(&frames)
free_frame(allocator, completed)
@@ -155,7 +157,7 @@ run proc(
}
.store_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
copy_value(&local.value, frames.items[frame_index].values[usize(instruction.left)])
copy_value(module, &local.value, frames.items[frame_index].values[usize(instruction.left)])
local.initialized = true
continue
}
@@ -164,7 +166,7 @@ run proc(
if (!local.initialized) {
return try runtime_trap(diagnostic_store, .write_before_initialization, instruction.span)
}
copy_value(&local.value, frames.items[frame_index].values[usize(instruction.left)])
copy_value(module, &local.value, frames.items[frame_index].values[usize(instruction.left)])
continue
}
.reset_local: {
@@ -199,7 +201,7 @@ run proc(
for (0..instruction.arguments_count) |argument_index| {
argument :: block.arguments.items[instruction.arguments_start + argument_index]
local :: &frames.items[frame_index + 1].locals[argument_index]
copy_value(&local.value, frames.items[frame_index].values[usize(argument)])
copy_value(module, &local.value, frames.items[frame_index].values[usize(argument)])
local.initialized = true
}
continue
@@ -212,7 +214,8 @@ run proc(
element :: frames.items[frame_index].values[usize(operand)]
if (ir.aggregate(element.type)) {
count :: element.data.elements.len
memcopy!(destination.data.elements[offset..offset + count], element.data.elements)
target :: aggregate_view(element.type, element.array_type, destination.data.elements[offset..offset + count])
ir.copy_aggregate(module, target, element)
offset += count
} else {
destination.data.elements[offset] = element
@@ -221,6 +224,18 @@ run proc(
}
continue
}
.union_literal: {
destination :: frames.items[frame_index].values[usize(instruction.destination)]
shape :: module.arrays.items[usize(instruction.array_type)]
mem.set(destination.data.elements, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } })
if (shape.tag_type != .void) ir.write_storage(module, destination.data.elements, instruction.constant)
if (instruction.left != ir.no_value) {
payload :: frames.items[frame_index].values[usize(instruction.left)]
ir.write_storage(module, destination.data.elements[shape.payload_offset..], payload)
}
continue
}
.union_tag: value = ir.union_tag(module, frames.items[frame_index].values[usize(instruction.left)])
.index: {
base :: frames.items[frame_index].values[usize(instruction.left)]
index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
@@ -232,6 +247,10 @@ run proc(
}
.field: {
base :: frames.items[frame_index].values[usize(instruction.left)]
shape :: module.arrays.items[usize(base.array_type)]
if (shape.is_union and !ir.union_active(module, base, usize(instruction.slot))) {
return try runtime_trap(diagnostic_store, .inactive_union_field, instruction.span)
}
value = ir.aggregate_item(module, base, usize(instruction.slot))
}
.address_local: {
@@ -284,26 +303,37 @@ run proc(
if (index >= u64(shape.len)) {
return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span)
}
width :: element_cells(module, shape)
width :: if (base.raw) ir.storage_size(module, shape.element, shape.element_array) else element_cells(module, shape)
start :: usize(index) * width
address := Address{
type = instruction.pointee, array_type = instruction.array_type,
cells = base.cells[start..start + width], initialized = base.initialized,
cells = base.cells[start..start + width], initialized = base.initialized, raw = base.raw,
}
if (!ir.aggregate(address.type)) address.scalar = &address.cells[0]
if (!address.raw and !ir.aggregate(address.type)) address.scalar = &address.cells[0]
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } }
}
.field_address: {
base_id :: frames.items[frame_index].values[usize(instruction.left)].data.address
base :: frames.items[frame_index].addresses[usize(base_id)]
field :: module.arrays.items[usize(base.array_type)].fields[usize(instruction.slot)]
width :: if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
shape :: module.arrays.items[usize(base.array_type)]
field :: shape.fields[usize(instruction.slot)]
if (shape.is_union and shape.tag_type != .void) {
if base.initialized |initialized| {
if (!initialized^) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
}
if (!ir.union_active(module, aggregate_view(.record, base.array_type, base.cells), usize(instruction.slot))) {
return try runtime_trap(diagnostic_store, .inactive_union_field, instruction.span)
}
}
raw :: base.raw or shape.is_union
width :: if (raw) ir.storage_size(module, field.type, field.array_type) else if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
start :: if (raw) field.byte_offset else field.offset
address := Address{
type = instruction.pointee, array_type = instruction.array_type,
cells = base.cells[field.offset..field.offset + width], initialized = base.initialized,
cells = base.cells[start..start + width], initialized = base.initialized, raw = raw,
}
if (!ir.aggregate(address.type)) address.scalar = &address.cells[0]
if (!raw and !ir.aggregate(address.type)) address.scalar = &address.cells[0]
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } }
}
@@ -317,10 +347,12 @@ run proc(
}
}
if (instruction.op == .load_address) {
value = if (ir.aggregate(address.type)) aggregate_view(address.type, address.array_type, address.cells) else address.scalar?^
if (address.raw) value = ir.read_storage(module, address.type, address.array_type, address.cells, true)
else value = if (ir.aggregate(address.type)) aggregate_view(address.type, address.array_type, address.cells) else address.scalar?^
} else {
source :: frames.items[frame_index].values[usize(instruction.right)]
if (ir.aggregate(address.type)) memcopy!(address.cells, source.data.elements)
if (address.raw) ir.write_storage(module, address.cells, source)
else if (ir.aggregate(address.type)) ir.copy_aggregate(module, aggregate_view(address.type, address.array_type, address.cells), source)
else address.scalar?^ = source
continue
}
@@ -421,7 +453,7 @@ run proc(
}
}
if instruction.destination != ir.no_value {
copy_value(&frames.items[frame_index].values[usize(instruction.destination)], value)
copy_value(module, &frames.items[frame_index].values[usize(instruction.destination)], value)
}
}
unreachable
@@ -518,10 +550,10 @@ aggregate_view proc(type ir.Type, shape ir.ArrayId, cells []mut ir.Value) ir.Val
}
@hide:file
copy_value proc(destination @mut ir.Value, source ir.Value) void {
copy_value proc(module @ir.Module, destination @mut ir.Value, source ir.Value) void {
if (ir.aggregate(source.type)) {
# aggregate destinations already own disjoint fixed segments; never install an alias.
memcopy!(destination.data.elements, source.data.elements)
ir.copy_aggregate(module, destination^, source)
} else {
destination^ = source
}
+123
View File
@@ -1463,3 +1463,126 @@ inclusive_unsigned_range_reaches_the_maximum_without_incrementing_past_it test {
try testing.expect(outcome.trap == null)
try testing.expect_equal(u64(2), outcome.value.data.unsigned)
}
union_storage_overlays_nested_record_and_array_payloads_and_copies_by_value test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
record_fields [2]ir.RecordField :: [ir.RecordField{ type = .u8 }, ir.RecordField{ type = .u32 }]
record :: try ir.add_record(&module, record_fields[..])
bytes :: try add_array(&module, .u8, ir.no_array, 8)
variants [2]ir.RecordField :: [
ir.RecordField{ type = .record, array_type = record },
ir.RecordField{ type = .array, array_type = bytes },
]
shape :: try ir.add_union(&module, variants[..], false)
identity :: try add_function(&module, &[], 0, .record)
_ = try ir.add_parameter(&module, identity, .record, shape)
module.functions.items[usize(identity)].result_array_type = shape
try emit(&module, identity, ir.Instruction{ op = .load_local, type = .record, array_type = shape, destination = ir.ValueId(0) })
return_value(&module, identity, ir.ValueId(0))
main :: try add_function(&module, &[], 0, .u32)
_ = try ir.add_local(&module, main, .record, shape)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .constant, type = .u8, destination = ir.ValueId(0), constant = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 1 } } })
try emit(&module, main, ir.Instruction{ op = .constant, type = .u32, destination = ir.ValueId(1), constant = ir.Value{ type = .u32, data = ir.ValueData{ unsigned = 287454020 } } })
block :: &module.functions.items[usize(main)].blocks.items[0]
try arraylist.append(&block.arguments, ir.ValueId(0))
try arraylist.append(&block.arguments, ir.ValueId(1))
try emit(&module, main, ir.Instruction{ op = .record_literal, type = .record, array_type = record, destination = ir.ValueId(2), arguments_count = 2 })
try emit(&module, main, ir.Instruction{ op = .union_literal, type = .record, array_type = shape, destination = ir.ValueId(3), left = ir.ValueId(2) })
try emit(&module, main, ir.Instruction{ op = .store_local, left = ir.ValueId(3) })
try emit(&module, main, function_value(4, identity))
try arraylist.append(&block.arguments, ir.ValueId(3))
try emit(&module, main, ir.Instruction{ op = .call, type = .record, array_type = shape, destination = ir.ValueId(5), left = ir.ValueId(4), arguments_start = 2, arguments_count = 1 })
try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .record, array_type = shape, destination = ir.ValueId(6) })
try emit(&module, main, ir.Instruction{ op = .field_address, type = .address, pointee = .array, array_type = bytes, destination = ir.ValueId(7), left = ir.ValueId(6), slot = 1 })
try emit(&module, main, unsigned(8, 4))
try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .u8, destination = ir.ValueId(9), left = ir.ValueId(7), right = ir.ValueId(8) })
try emit(&module, main, ir.Instruction{ op = .constant, type = .u8, destination = ir.ValueId(10), constant = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 153 } } })
try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(9), right = ir.ValueId(10) })
try emit(&module, main, ir.Instruction{ op = .load_local, type = .record, array_type = shape, destination = ir.ValueId(11) })
try emit(&module, main, ir.Instruction{ op = .field, type = .record, array_type = record, destination = ir.ValueId(12), left = ir.ValueId(11) })
try emit(&module, main, ir.Instruction{ op = .field, type = .u32, destination = ir.ValueId(13), left = ir.ValueId(12), slot = 1 })
try emit(&module, main, ir.Instruction{ op = .field, type = .record, array_type = record, destination = ir.ValueId(14), left = ir.ValueId(5) })
try emit(&module, main, ir.Instruction{ op = .field, type = .u32, destination = ir.ValueId(15), left = ir.ValueId(14), slot = 1 })
try emit(&module, main, ir.Instruction{ op = .subtract, type = .u32, destination = ir.ValueId(16), left = ir.ValueId(13), right = ir.ValueId(15) })
return_value(&module, main, ir.ValueId(16))
outcome := try run(mem.c_allocator, &module, &store)
defer deinit(&outcome)
try testing.expect(outcome.trap == null)
try testing.expect_equal(u64(85), outcome.value.data.unsigned)
}
tagged_unions_preserve_actual_tags_and_trap_inactive_value_and_address_projections test {
operations [2]ir.Op :: [.field, .field_address]
for (operations) |operation| {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
tag :: ir.Value{ type = .i8, data = ir.ValueData{ integer = -1 } }
variants [2]ir.RecordField :: [
ir.RecordField{ type = .int, tag = tag },
ir.RecordField{ type = .int, tag = ir.Value{ type = .i8, data = ir.ValueData{ integer = 37 } } },
]
shape :: try ir.add_union(&module, variants[..], true)
main :: try add_function(&module, &[], 0, .i8)
_ = try ir.add_local(&module, main, .record, shape)
module.entry = main
try emit(&module, main, integer(0, 42))
try emit(&module, main, ir.Instruction{ op = .union_literal, type = .record, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0), constant = tag })
try emit(&module, main, ir.Instruction{ op = .union_tag, type = .i8, destination = ir.ValueId(2), left = ir.ValueId(1) })
return_value(&module, main, ir.ValueId(2))
outcome := try run(mem.c_allocator, &module, &store)
defer deinit(&outcome)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(-1), outcome.value.data.integer)
base := ir.ValueId(1)
if (operation == .field_address) {
try emit(&module, main, ir.Instruction{ op = .store_local, left = base })
try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .record, array_type = shape, destination = ir.ValueId(3) })
base = ir.ValueId(3)
}
span :: diagnostics.Span{ start = 11, end = 15 }
type :: if (operation == .field) ir.Type.int else ir.Type.address
try emit(&module, main, ir.Instruction{ op = operation, type = type, pointee = .int, destination = ir.ValueId(4), left = base, slot = 1, span = span })
trapped := try run(mem.c_allocator, &module, &store)
defer deinit(&trapped)
try expect_trap(trapped, &store, .inactive_union_field, span)
}
}
union_scalar_reinterpretation_and_zero_leaf_payloads_keep_their_shapes test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
empty :: try ir.add_record(&module, &[])
variants [3]ir.RecordField :: [
ir.RecordField{ type = .float },
ir.RecordField{ type = .u64 },
ir.RecordField{ type = .record, array_type = empty },
]
shape :: try ir.add_union(&module, variants[..], false)
main :: try add_function(&module, &[], 0, .u64)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 1.0 } } })
try emit(&module, main, ir.Instruction{ op = .union_literal, type = .record, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0) })
try emit(&module, main, ir.Instruction{ op = .field, type = .u64, destination = ir.ValueId(2), left = ir.ValueId(1), slot = 1 })
return_value(&module, main, ir.ValueId(2))
bits := try run(mem.c_allocator, &module, &store)
defer deinit(&bits)
try testing.expect(bits.trap == null)
try testing.expect_equal(u64(4607182418800017408), bits.value.data.unsigned)
try emit(&module, main, ir.Instruction{ op = .field, type = .record, array_type = empty, destination = ir.ValueId(3), left = ir.ValueId(1), slot = 2 })
module.functions.items[usize(main)].result = .record
module.functions.items[usize(main)].result_array_type = empty
return_value(&module, main, ir.ValueId(3))
zero := try run(mem.c_allocator, &module, &store)
defer deinit(&zero)
try testing.expect(zero.trap == null)
try testing.expect_equal(empty, zero.value.array_type)
try testing.expect_equal(usize(0), zero.value.data.elements.len)
}
+189 -9
View File
@@ -85,6 +85,7 @@ ValueData :: union {
Value :: struct {
type Type = .void
array_type ArrayId = no_array
raw bool = false
data ValueData = ValueData{ integer = 0 }
}
@@ -92,10 +93,12 @@ RecordField :: struct {
type Type
array_type ArrayId = no_array
offset usize = 0
byte_offset usize = 0
tag Value = Value{}
}
#! fixed recursive aggregate shape; constructors derive the flattened scalar-leaf count.
#! record fields are owned by the module; array metadata retains its existing representation.
#! fixed recursive aggregate shape; cells count interpreter slots, byte_size describes native layout.
#! unions retain overlapping byte cells; ordinary aggregates retain flattened scalar leaves.
ArrayType :: struct {
kind Type = .array
element Type = .invalid
@@ -104,6 +107,11 @@ ArrayType :: struct {
cells usize = 0
mutable bool = false
fields []mut RecordField = &[]
is_union bool = false
tag_type Type = .void
payload_offset usize = 0
byte_size usize = 0
alignment usize = 1
}
#! maps a local slot or temporary to its preallocated, by-value frame segment.
@@ -123,6 +131,8 @@ Op :: enum {
load_global
array_literal
record_literal
union_literal
union_tag
field
index
address_local
@@ -167,6 +177,7 @@ TerminatorKind :: enum {
trap,
jump,
branch,
unreachable,
}
Terminator :: struct {
@@ -266,6 +277,8 @@ add_array proc(
cells :: len * width
if (cells > divtrunc!(maxval!(usize), sizeof!(Value))) return .storage_limit_reached
if (module.arrays.items.len >= usize(no_array)) return .storage_limit_reached
stride :: storage_size(module, element, element_array)
if (stride != 0 and len > divtrunc!(maxval!(usize), stride)) return .storage_limit_reached
id :: ArrayId(module.arrays.items.len)
try arraylist.append(&module.arrays, ArrayType{
element = element,
@@ -273,6 +286,8 @@ add_array proc(
len = len,
cells = cells,
mutable = mutable,
byte_size = len * stride,
alignment = storage_alignment(module, element, element_array),
})
return id
}
@@ -286,19 +301,35 @@ add_record proc(
owned :: try mem.alloc(RecordField, module.allocator, fields.len)
errdefer mem.free(module.allocator, owned)
cells usize := 0
bytes usize := 0
alignment usize := 1
for (fields) |field, index| {
debug.assert(aggregate(field.type) == (field.array_type != no_array))
width :: if (aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
if (width > divtrunc!(maxval!(usize), sizeof!(Value)) - cells) return .storage_limit_reached
owned[index] = RecordField{ type = field.type, array_type = field.array_type, offset = cells }
field_alignment :: storage_alignment(module, field.type, field.array_type)
if (field_alignment > alignment) alignment = field_alignment
if (bytes > maxval!(usize) - (field_alignment - 1)) return .storage_limit_reached
bytes = aligned_size(bytes, field_alignment)
size :: storage_size(module, field.type, field.array_type)
if (size > maxval!(usize) - bytes) return .storage_limit_reached
owned[index] = RecordField{
type = field.type, array_type = field.array_type, offset = cells,
byte_offset = bytes, tag = field.tag,
}
bytes += size
cells += width
}
id :: ArrayId(module.arrays.items.len)
try arraylist.append(&module.arrays, ArrayType{ kind = .record, fields = owned, cells = cells })
if (bytes > maxval!(usize) - (alignment - 1)) return .storage_limit_reached
try arraylist.append(&module.arrays, ArrayType{
kind = .record, fields = owned, cells = cells,
byte_size = aligned_size(bytes, alignment), alignment = alignment,
})
return id
}
#! returns a borrowed child value from flattened aggregate storage, preserving zero-cell shapes.
#! returns a borrowed child, including a raw view when projecting overlapping union storage.
aggregate_item proc(module @Module, value Value, index usize) Value {
shape :: module.arrays.items[usize(value.array_type)]
type := shape.element
@@ -310,13 +341,162 @@ aggregate_item proc(module @Module, value Value, index usize) Value {
type = field.type
child = field.array_type
start = field.offset
if (value.raw) start = field.byte_offset
if (shape.is_union) start = shape.payload_offset
} else {
if (aggregate(type)) width = module.arrays.items[usize(child)].cells
if (value.raw) width = storage_size(module, type, child)
else if (aggregate(type)) width = module.arrays.items[usize(child)].cells
start = index * width
}
if (!aggregate(type)) return value.data.elements[start]
width = module.arrays.items[usize(child)].cells
return Value{ type = type, array_type = child, data = ValueData{ elements = value.data.elements[start..start + width] } }
raw :: value.raw or shape.is_union
if (!aggregate(type)) {
if (raw) return read_storage(module, type, child, value.data.elements[start..], true)
return value.data.elements[start]
}
width = if (raw) storage_size(module, type, child) else module.arrays.items[usize(child)].cells
return Value{
type = type, array_type = child, raw = raw,
data = ValueData{ elements = value.data.elements[start..start + width] },
}
}
#! copies variant descriptors; all payloads share one eight-byte-aligned storage region.
add_union proc(module @mut Module, fields []RecordField, tagged bool) ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (module.arrays.items.len >= usize(no_array)) return .storage_limit_reached
owned :: try mem.alloc(RecordField, module.allocator, fields.len)
errdefer mem.free(module.allocator, owned)
size usize := 0
for (fields) |field, index| {
width :: storage_size(module, field.type, field.array_type)
if (width > size) size = width
owned[index] = field
owned[index].offset = if (tagged) 8 else 0
owned[index].byte_offset = owned[index].offset
}
prefix usize :: if (tagged) 8 else 0
if (size > divtrunc!(maxval!(usize), sizeof!(Value)) - 15) return .storage_limit_reached
size = aligned_size(size, 8) + prefix
id :: ArrayId(module.arrays.items.len)
tag_type :: if (tagged and fields.len != 0) fields[0].tag.type else Type.void
try arraylist.append(&module.arrays, ArrayType{
kind = .record, fields = owned, is_union = true,
tag_type = tag_type,
payload_offset = prefix, cells = size, byte_size = size, alignment = 8,
})
return id
}
@hide:file
aligned_size proc(size, alignment usize) usize {
return divtrunc!(size + alignment - 1, alignment) * alignment
}
storage_alignment proc(module @Module, type Type, shape ArrayId) usize {
if (aggregate(type)) return module.arrays.items[usize(shape)].alignment
bits :: integer_bits(type)
if (bits != 0) return divtrunc!(bits, 8)
return if (type == .float or type == .string or type == .function) 8 else 1
}
storage_size proc(module @Module, type Type, shape ArrayId) usize {
if (aggregate(type)) return module.arrays.items[usize(shape)].byte_size
bits :: integer_bits(type)
if (bits != 0) return divtrunc!(bits, 8)
return match type {
.float, .function: 8
.string: 16
.bool: 1
.void: 0
else: unreachable
}
}
#! loads a scalar or borrows an aggregate from byte cells without allocating.
read_storage proc(module @Module, type Type, shape ArrayId, cells []mut Value, raw bool) Value {
if (aggregate(type)) {
return Value{ type = type, array_type = shape, raw = raw, data = ValueData{ elements = cells[..storage_size(module, type, shape)] } }
}
bits u64 := 0
size :: storage_size(module, type, shape)
count :: if (size > 8) usize(8) else size
for (0..count) |i| bits += cells[i].data.unsigned << u64(i * 8)
data := ValueData{ unsigned = bits }
match type {
.i8: data = ValueData{ integer = i64(ptrcast!(@i8, &bits)^) }
.i16: data = ValueData{ integer = i64(ptrcast!(@i16, &bits)^) }
.i32: data = ValueData{ integer = i64(ptrcast!(@i32, &bits)^) }
.int, .isize: data = ValueData{ integer = ptrcast!(@i64, &bits)^ }
.float: data = ValueData{ real = ptrcast!(@f64, &bits)^ }
.bool: data = ValueData{ boolean = (bits & 1) != 0 }
.string: data = ValueData{ string = StringId(bits) }
.function: data = ValueData{ function = FunctionId(bits) }
.void: return Value{}
else: {}
}
return Value{ type = type, data = data }
}
#! stores a value in shared byte cells; recursive aggregates preserve their native field padding.
write_storage proc(module @Module, cells []mut Value, value Value) void {
if (aggregate(value.type)) {
shape :: module.arrays.items[usize(value.array_type)]
if (value.raw or shape.is_union) {
memcopy!(cells[..shape.byte_size], value.data.elements)
return
}
count :: if (shape.kind == .record) shape.fields.len else shape.len
for (0..count) |i| {
offset :: if (shape.kind == .record) shape.fields[i].byte_offset else i * storage_size(module, shape.element, shape.element_array)
write_storage(module, cells[offset..], aggregate_item(module, value, i))
}
return
}
bits u64 := 0
match value.type {
.float: bits = ptrcast!(@u64, &value.data.real)^
.bool: bits = if (value.data.boolean) 1 else 0
.string: bits = u64(value.data.string)
.function: bits = u64(value.data.function)
.void: return
else: bits = ptrcast!(@u64, &value.data.integer)^
}
size :: storage_size(module, value.type, value.array_type)
for (0..size) |i| {
byte u64 := 0
if (i < 8) byte = (bits >> u64(i * 8)) & 255
else byte = (u64(string_bytes(module, value.data.string).len) >> u64((i - 8) * 8)) & 255
cells[i] = Value{ type = .u8, data = ValueData{ unsigned = byte } }
}
}
#! converts raw aggregate projections into the caller's preallocated flattened segment.
copy_aggregate proc(module @Module, destination Value, source Value) void {
shape :: module.arrays.items[usize(source.array_type)]
if (!source.raw or shape.is_union) {
memcopy!(destination.data.elements, source.data.elements)
return
}
count :: if (shape.kind == .record) shape.fields.len else shape.len
for (0..count) |i| {
child :: aggregate_item(module, source, i)
if (aggregate(child.type)) copy_aggregate(module, aggregate_item(module, destination, i), child)
else {
offset :: if (shape.kind == .record) shape.fields[i].offset else i
destination.data.elements[offset] = child
}
}
}
union_tag proc(module @Module, value Value) Value {
shape :: module.arrays.items[usize(value.array_type)]
return read_storage(module, shape.tag_type, no_array, value.data.elements, true)
}
union_active proc(module @Module, value Value, field usize) bool {
shape :: module.arrays.items[usize(value.array_type)]
if (shape.tag_type == .void) return true
tag :: union_tag(module, value)
return tag.data.unsigned == shape.fields[field].tag.data.unsigned
}
@hide:file
+7 -6
View File
@@ -34,6 +34,11 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "or", .or },
{ "mut", .mut },
{ "struct", .struct },
{ "enum", .enum },
{ "union", .union },
{ "distinct", .distinct },
{ "alias", .alias },
{ "match", .match },
{ "import", .import },
{ "test", .test },
])
@@ -85,7 +90,6 @@ scan proc(
if char == '#' {
while (cursor < program.len and program[cursor] != '\n') cursor += 1
if (cursor < program.len) cursor += 1
continue
}
@@ -352,12 +356,9 @@ scan_ident proc(
cursor += 1
}
# don't intern keywords (already O(1) lookup via token kind)
kind :: strmap.get(&keywords, program[start..cursor]) orelse .ident
str_id :: if (kind == .ident)
try strpool.intern(pool, program[start..cursor])
else
strpool.no_str
# member contexts accept keywords and therefore need their interned spelling too.
str_id :: try strpool.intern(pool, program[start..cursor])
return ScanIdentResult{ end = cursor, kind = kind, str_id = str_id }
}
+8 -11
View File
@@ -13,15 +13,16 @@ reports_invalid_input_and_keeps_scanning test {
try scan(&state, "if name # comment\n$1. 2 3.5 \"hi\"", &pool, &diagnostic_store)
try testing.expect_equal(8, state.tokens.items.len)
try testing.expect_equal(9, state.tokens.items.len)
try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind)
try testing.expect_equal(TokenKind.ident, state.tokens.items[1].kind)
try testing.expect_equal(TokenKind.invalid, state.tokens.items[2].kind)
try testing.expect_equal(TokenKind.newline, state.tokens.items[2].kind)
try testing.expect_equal(TokenKind.invalid, state.tokens.items[3].kind)
try testing.expect_equal(TokenKind.int, state.tokens.items[4].kind)
try testing.expect_equal(TokenKind.float, state.tokens.items[5].kind)
try testing.expect_equal(TokenKind.string, state.tokens.items[6].kind)
try testing.expect_equal(TokenKind.eof, state.tokens.items[7].kind)
try testing.expect_equal(TokenKind.invalid, state.tokens.items[4].kind)
try testing.expect_equal(TokenKind.int, state.tokens.items[5].kind)
try testing.expect_equal(TokenKind.float, state.tokens.items[6].kind)
try testing.expect_equal(TokenKind.string, state.tokens.items[7].kind)
try testing.expect_equal(TokenKind.eof, state.tokens.items[8].kind)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
try testing.expect_equal(
"name",
@@ -142,7 +143,7 @@ scans_array_delimiters_and_mut_keyword test {
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
reuses_identifiers_without_interning_keywords test {
reuses_identifier_spellings test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
@@ -155,8 +156,6 @@ reuses_identifiers_without_interning_keywords test {
try testing.expect(first != second)
try testing.expect_equal(first, state.tokens.items[2].data.str_id)
try testing.expect_equal(second, state.tokens.items[4].data.str_id)
try testing.expect_equal(null, strpool.get_id(&pool, "if"))
try testing.expect_equal(usize(2), pool.strings.items.len)
}
@hide:file
@@ -230,8 +229,6 @@ scans_import_keywords_and_preserves_source_in_tokens_and_diagnostics test {
try scan(&state, "import test $\n1.\n\"unfinished", &pool, &diagnostic_store)
try testing.expect_equal(TokenKind.import, state.tokens.items[0].kind)
try testing.expect_equal(TokenKind.test, state.tokens.items[1].kind)
try testing.expect_equal(null, strpool.get_id(&pool, "import"))
try testing.expect_equal(null, strpool.get_id(&pool, "test"))
for (state.tokens.items) |token| try testing.expect_equal(state.source_id, token.span.source)
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
for (diagnostic_store.list.items) |diagnostic| {
+4 -2
View File
@@ -20,13 +20,15 @@ TokenKind :: enum {
# keywords
if, else
for, while
match
proc, return
break, continue
true, false
and, or
mut
struct
struct, enum, union
distinct, alias
import, test
mut
# literals
ident, string
+97 -10
View File
@@ -1,5 +1,6 @@
import "@std/io"
import "@source/ir"
import "@source/diagnostics"
@hide
emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruction) void ! io.WriteError {
@@ -57,11 +58,49 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
}
return
}
.union_literal: {
shape :: state.module.arrays.items[usize(instruction.array_type)]
try io.write_all(output, " store ")
try write_type(output, state.module, type)
try io.print(" zeroinitializer, ptr %v{}\n", output, {u32(instruction.destination)})
if (shape.tag_type != .void) {
try io.write_all(output, " store ")
try write_type(output, state.module, Type{ kind = shape.tag_type })
try io.write_all(output, " ")
try write_constant(output, state.module, instruction.constant)
try io.print(", ptr %v{}\n", output, {u32(instruction.destination)})
}
if (instruction.left != ir.no_value) {
payload :: try load(state, instruction.left)
pointer :: fresh(state)
try io.print(" %r{} = getelementptr i8, ptr %v{}, i64 {}\n", output, {pointer, u32(instruction.destination), shape.payload_offset})
try store_payload(state, state.values[usize(instruction.left)], payload, pointer)
}
return
}
.union_tag: {
try io.print(" %r{} = load ", output, {result})
try write_type(output, state.module, type)
try io.print(", ptr %v{}\n", output, {u32(instruction.left)})
}
.field: {
base :: try load(state, instruction.left)
try io.print(" %r{} = extractvalue ", output, {result})
try write_type(output, state.module, state.values[usize(instruction.left)])
try io.print(" %r{}, {}\n", output, {base, instruction.slot})
base_type :: state.values[usize(instruction.left)]
shape :: state.module.arrays.items[usize(base_type.shape)]
if (shape.is_union) {
pointer :: fresh(state)
try io.print(" %r{} = getelementptr i8, ptr %v{}, i64 0\n", output, {pointer, u32(instruction.left)})
try check_union(state, pointer, shape, usize(instruction.slot), instruction.span)
if (instruction.type == .void) return
payload :: fresh(state)
try io.print(" %r{} = getelementptr i8, ptr %r{}, i64 {}\n %r{} = load ", output, {payload, pointer, shape.payload_offset, result})
try write_type(output, state.module, type)
try io.print(", ptr %r{}\n", output, {payload})
} else {
base :: try load(state, instruction.left)
try io.print(" %r{} = extractvalue ", output, {result})
try write_type(output, state.module, base_type)
try io.print(" %r{}, {}\n", output, {base, instruction.slot})
}
}
.field_address: {
base_type :: state.values[usize(instruction.left)]
@@ -72,9 +111,19 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
address :: fresh(state)
try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {data, base})
try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, base})
try io.print(" %r{} = getelementptr ", output, {pointer})
try write_type(output, state.module, Type{ kind = .record, shape = base_type.shape })
try io.print(", ptr %r{}, i64 0, i32 {}\n", output, {data, instruction.slot})
shape :: state.module.arrays.items[usize(base_type.shape)]
if (shape.is_union) {
if (shape.tag_type != .void) {
position :: location(state.line_starts, instruction.span)
try io.print(" call void @check_init(ptr %r{}, i1 true, i64 {}, i64 {}, i64 {})\n", output, {flag, usize(instruction.span.source), position.line, position.column})
try check_union(state, data, shape, usize(instruction.slot), instruction.span)
}
try io.print(" %r{} = getelementptr i8, ptr %r{}, i64 {}\n", output, {pointer, data, shape.payload_offset})
} else {
try io.print(" %r{} = getelementptr ", output, {pointer})
try write_type(output, state.module, Type{ kind = .record, shape = base_type.shape })
try io.print(", ptr %r{}, i64 0, i32 {}\n", output, {data, instruction.slot})
}
try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer})
try io.print(" %r{} = insertvalue %address %r{}, ptr %r{}, 1\n", output, {result, address, flag})
}
@@ -129,9 +178,7 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
try io.print(" call void @check_init(ptr %r{}, i1 {}, i64 {}, i64 {}, i64 {})\n", output, {flag, instruction.op == .load_address, usize(instruction.span.source), position.line, position.column})
if (instruction.op == .store_address) {
value :: try load(state, instruction.right)
try io.write_all(output, " store ")
try write_type(output, state.module, state.values[usize(instruction.right)])
try io.print(" %r{}, ptr %r{}\n", output, {value, pointer})
try store_payload(state, state.values[usize(instruction.right)], value, pointer)
return
}
try io.print(" %r{} = load ", output, {result})
@@ -246,6 +293,46 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
try store(state, instruction.destination, result)
}
@hide:file
check_union proc(state @mut State, pointer usize, shape ir.ArrayType, field usize, span diagnostics.Span) void ! io.WriteError {
if (shape.tag_type == .void) return
tag :: fresh(state)
failed :: fresh(state)
try io.print(" %r{} = load ", state.output, {tag})
try write_type(state.output, state.module, Type{ kind = shape.tag_type })
try io.print(", ptr %r{}\n %r{} = icmp ne ", state.output, {pointer, failed})
try write_type(state.output, state.module, Type{ kind = shape.tag_type })
try io.print(" %r{}, ", state.output, {tag})
try write_constant(state.output, state.module, shape.fields[field].tag)
try io.write_all(state.output, "\n")
try check_failure(state, failed, .inactive_union_field, span)
}
@hide:file
store_payload proc(state @mut State, type Type, value, pointer usize) void ! io.WriteError {
if (type.kind == .void) return
if (ir.aggregate(type.kind)) {
shape :: state.module.arrays.items[usize(type.shape)]
if (!shape.is_union) {
count usize :: if (type.kind == .record) shape.fields.len else shape.len
for (0..count) |i| {
child :: if (type.kind == .record) Type{ kind = shape.fields[i].type, shape = shape.fields[i].array_type } else Type{ kind = shape.element, shape = shape.element_array }
offset usize :: if (type.kind == .record) shape.fields[i].byte_offset else i * ir.storage_size(state.module, shape.element, shape.element_array)
element :: fresh(state)
address :: fresh(state)
try io.print(" %r{} = extractvalue ", state.output, {element})
try write_type(state.output, state.module, type)
try io.print(" %r{}, {}\n %r{} = getelementptr i8, ptr %r{}, i64 {}\n", state.output, {value, i, address, pointer, offset})
try store_payload(state, child, element, address)
}
return
}
}
try io.write_all(state.output, " store ")
try write_type(state.output, state.module, type)
try io.print(" %r{}, ptr %r{}\n", state.output, {value, pointer})
}
@hide:file
emit_conversion proc(state @mut State, instruction ir.Instruction) usize ! io.WriteError {
output :: state.output
+67
View File
@@ -143,6 +143,11 @@ write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteE
}
.record: {
shape :: module.arrays.items[usize(type.shape)]
if (shape.is_union) {
words usize :: divtrunc!(shape.byte_size, 8)
try io.print("{{ [{} x i64] }}", output, {words})
return
}
try io.write_all(output, "{ ")
for (shape.fields) |field, index| {
if (index != 0) try io.write_all(output, ", ")
@@ -307,6 +312,10 @@ emit_array_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void
emit_record_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void ! io.WriteError {
shape :: module.arrays.items[usize(id)]
type :: Type{ kind = .record, shape = id }
if (shape.is_union) {
try emit_union_printer(output, module, id)
return
}
try io.print("define internal void @print_array{}(", output, {u32(id)})
try write_type(output, module, type)
try io.write_all(output, " %value) {\nentry:\n call void @out(i32 1, ptr @record_open, i64 1)\n")
@@ -321,6 +330,51 @@ emit_record_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) voi
try io.write_all(output, " call void @out(i32 1, ptr @record_close, i64 1)\n ret void\n}\n")
}
@hide:file
emit_union_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void ! io.WriteError {
shape :: module.arrays.items[usize(id)]
type :: Type{ kind = .record, shape = id }
try io.print("define internal void @print_array{}(", output, {u32(id)})
try write_type(output, module, type)
try io.write_all(output, " %value) {\nentry:\n %storage = alloca ")
try write_type(output, module, type)
try io.write_all(output, "\n store ")
try write_type(output, module, type)
try io.write_all(output, " %value, ptr %storage\n call void @out(i32 1, ptr @record_open, i64 1)\n")
if (shape.tag_type != .void) {
try io.write_all(output, " %tag = load ")
try write_type(output, module, Type{ kind = shape.tag_type })
try io.write_all(output, ", ptr %storage\n")
target :: if (shape.fields.len == 0) " br label %end\n" else " br label %test0\n"
try io.write_all(output, target)
for (shape.fields) |field, index| {
try io.print("test{}:\n %tag_match{} = icmp eq ", output, {index, index})
try write_type(output, module, Type{ kind = shape.tag_type })
try io.write_all(output, " %tag, ")
try write_constant(output, module, field.tag)
try io.print("\n br i1 %tag_match{}, label %case{}, label %", output, {index, index})
if (index + 1 == shape.fields.len) try io.write_all(output, "end\n")
else try io.print("test{}\n", output, {index + 1})
}
} else {
target :: if (shape.fields.len == 0) " br label %end\n" else " br label %case0\n"
try io.write_all(output, target)
}
for (shape.fields) |field, index| {
if (shape.tag_type == .void and index != 0) break
try io.print("case{}:\n", output, {index})
if (field.type != .void) {
try io.print(" %p{} = getelementptr i8, ptr %storage, i64 {}\n %r{} = load ", output, {index, shape.payload_offset, index})
try write_type(output, module, Type{ kind = field.type, shape = field.array_type })
try io.print(", ptr %p{}\n", output, {index})
if (field.type != .function) try print_call(output, module, Type{ kind = field.type, shape = field.array_type }, index)
else try io.write_all(output, " call void @exit(i32 2)\n")
}
try io.write_all(output, " br label %end\n")
}
try io.write_all(output, "end:\n call void @out(i32 1, ptr @record_close, i64 1)\n ret void\n}\n")
}
@hide
fresh proc(state @mut State) usize {
value :: state.next_register
@@ -357,6 +411,7 @@ trap proc(state @State, code diagnostics.Code, span diagnostics.Span) void ! io.
symbol []u8 :: match code {
.integer_overflow: "overflow"
.index_out_of_bounds: "bounds"
.inactive_union_field: "inactive_union"
else: unreachable
}
message :: enummap.get(diagnostics.Code, []u8, &diagnostics.messages, code)?
@@ -398,6 +453,17 @@ write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void !
}
.record: {
shape :: module.arrays.items[usize(value.array_type)]
if (shape.is_union) {
words usize :: divtrunc!(shape.byte_size, 8)
try io.print("{{ [{} x i64] [", output, {words})
for (0..words) |index| {
if (index != 0) try io.write_all(output, ", ")
word :: ir.read_storage(module, .u64, ir.no_array, value.data.elements[index * 8..], true)
try io.print("i64 {}", output, {word.data.unsigned})
}
try io.write_all(output, "] }")
return
}
try io.write_all(output, "{ ")
for (shape.fields) |_, index| {
if (index != 0) try io.write_all(output, ", ")
@@ -457,6 +523,7 @@ emit_function proc(state @mut State, function @ir.Function, id usize) void ! io.
}
}
.trap: try io.print(" call void @trap(i64 {}, i64 0, i64 0, i64 0, ptr null, i64 0)\n unreachable\n", output, {u32(terminator.diagnostic)})
.unreachable: try io.write_all(output, " unreachable\n")
.jump: try io.print(" br label %b{}\n", output, {u32(terminator.target)})
.branch: {
condition :: try load(state, terminator.value)
+1
View File
@@ -52,6 +52,7 @@ runtime ::
`@cycle = private constant [27 x i8] c"global initialization cycle"
`@overflow = private constant [27 x i8] c"integer arithmetic overflow"
`@bounds = private constant [19 x i8] c"index out of bounds"
`@inactive_union = private constant [32 x i8] c"tagged union field is not active"
`define internal void @out(i32 %fd, ptr %bytes, i64 %len) {
`entry:
` br label %loop
+242 -23
View File
@@ -102,10 +102,11 @@ lower_package proc(
}
.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))
global :: u32(module.globals.items.len)
try arraylist.append(&module.globals, ir.Global{
type = lower_type(typecheck.type_of(checked, id)),
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),
@@ -121,6 +122,7 @@ lower_package proc(
match ast.decl_kind(tree, id) {
.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: {}
@@ -130,7 +132,10 @@ lower_package proc(
}
@hide:file
lower_type proc(type typecheck.Type) ir.Type {
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 })
}
return match type.kind {
.int: .int
.usize: .usize
@@ -149,6 +154,7 @@ lower_type proc(type typecheck.Type) ir.Type {
.string: .string
.void: .void
.function: .function
.enumeration: unreachable
.none, .invalid, .range, .type_value: .invalid
}
}
@@ -169,7 +175,7 @@ prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.All
if (descriptor.element.kind == .array or descriptor.element.kind == .record) child = try array_shape(state, descriptor.element)
result :: try ir.add_array(
state.module,
lower_type(descriptor.element),
lower_type(state, descriptor.element),
child,
descriptor.len,
descriptor.mutable,
@@ -186,12 +192,24 @@ prepare_record proc(state @mut State, id typecheck.RecordId) ir.ArrayId ! (mem.A
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(field.type),
type = lower_type(state, field.type),
array_type = try array_shape(state, field.type),
tag = tag,
})
}
shape :: try ir.add_record(state.module, fields.items)
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
}
@@ -225,7 +243,7 @@ find_entry proc(state @mut State, root_package ast.NodeId) void ! (mem.AllocErro
return
}
declaration :: ast.function_decl(tree, id)
result :: lower_type(typecheck.type_of(state.checked, declaration.return_type))
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
@@ -308,7 +326,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
state.function = usize(state.storage[usize(id)].index)
state.block = ir.BlockId(0)
state.terminated = false
state.module.functions.items[state.function].result = lower_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))
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
@@ -316,7 +334,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
local :: try ir.add_parameter(
state.module,
ir.FunctionId(state.function),
lower_type(type),
lower_type(state, type),
try array_shape(state, type),
)
state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(local) }
@@ -344,11 +362,12 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro
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(type),
lower_type(state, type),
try array_shape(state, type),
)
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(local) }
@@ -362,7 +381,7 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro
.for_stmt: {
loop :: ast.for_stmt(state.tree, statement)
family :: typecheck.type_of(state.checked, loop.iterable).range_element
type :: lower_type(typecheck.Type{ kind = family })
type :: lower_type(state, typecheck.Type{ kind = family })
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| state.storage[usize(capture)] = state.storage[usize(statement)]
@@ -385,6 +404,7 @@ lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (m
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,
@@ -498,7 +518,7 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos
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(typecheck.Type{ kind = typecheck.type_of(state.checked, loop.iterable).range_element })
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)
@@ -596,7 +616,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
binding :: sema.binding(state.resolution, assignment.target)
_ = try emit(state, ir.Instruction{
op = .assign_local,
type = lower_type(typecheck.type_of(state.checked, assignment.target)),
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,
@@ -610,6 +630,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
.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
@@ -617,7 +638,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
if (state.terminated or trap_type(state, id)) return
_ = try emit(state, ir.Instruction{
op = .store_local,
type = lower_type(typecheck.type_of(state.checked, id)),
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,
@@ -676,7 +697,186 @@ lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError
}
@hide:file
lower_record proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
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 (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) }
}
if (!arm_view.expression) try predeclare_locals(state, arm_view.body)
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 (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_expression(state, arm_view.body)
if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{
op = .store_local, left = value, slot = result_slot, span = span,
})
} else try lower_block(state, arm_view.body, 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_expression(state, ast.field(tree, ast.record_field(tree, id, 0)).value?)
}
}
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]
@@ -689,7 +889,6 @@ lower_record proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError
state.arguments.items[scratch_start + i] = value
}
if (trap_type(state, id)) return ir.no_value
type :: typecheck.type_of(state.checked, id)
descriptor :: typecheck.record_type(state.checked, type.record_type)
ordered_start :: state.arguments.items.len
try arraylist.reserve(&state.arguments, ordered_start + descriptor.fields.len)
@@ -758,7 +957,7 @@ lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError |
return try emit(state, ir.Instruction{
op = .field_address,
type = .address,
pointee = lower_type(checked),
pointee = lower_type(state, checked),
array_type = try array_shape(state, checked),
left = base,
slot = u32(typecheck.field_index(state.checked, id)),
@@ -774,7 +973,7 @@ lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError |
return try emit(state, ir.Instruction{
op = .index_address,
type = .address,
pointee = lower_type(checked),
pointee = lower_type(state, checked),
array_type = try array_shape(state, checked),
left = base,
right = index,
@@ -788,7 +987,7 @@ lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError |
return try emit(state, ir.Instruction{
op = operation,
type = .address,
pointee = lower_type(checked),
pointee = lower_type(state, checked),
array_type = try array_shape(state, checked),
slot = location.index,
span = span,
@@ -799,8 +998,13 @@ lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError |
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(typecheck.type_of(state.checked, 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) {
.invalid: {
invalid :: ast.invalid(tree, id)
@@ -810,12 +1014,23 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
# ranges have no physical value; only lower_for decomposes an iterable.
if (!trap_type(state, id)) unreachable
}
.array_type, .record_type: {
.array_type, .record_type, .enum_type, .union_type, .distinct_type: {
if (!trap_type(state, id)) {
trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span)
}
}
.record_literal: return try lower_record(state, id)
.alias_expr: return try lower_expression(state, ast.type_operand(tree, id))
.enum_literal: {
if (!trap_type(state, id)) unreachable
}
.match_expr: return try lower_match(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]
@@ -948,7 +1163,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
}
.call: {
target :: sema.binding(state.resolution, ast.call_expr(tree, id).callee)
if (target.kind == .builtin) {
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))
@@ -956,6 +1171,10 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
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)
@@ -1018,7 +1237,7 @@ exact_float_integer proc(text []u8, value f64) bool {
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span)
type :: lower_type(typecheck.type_of(state.checked, 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, ir.integer_max(type))) |magnitude| {
return try constant(state, ir.Value{ type = type, data = ir.ValueData{ integer = i64(magnitude) } }, span)
+4 -4
View File
@@ -132,14 +132,14 @@ executes_forward_lazy_globals_and_function_aliases test {
defer diagnostics.deinit(&diagnostic_store)
source ::
`forward :: later + 1
`alias :: add
`forwarded :: add
`later :: 18
`add proc(left int, right int) int {
` local_alias :: alias
` local_alias :: forwarded
` return left + right
`}
`main proc() int {
` invoke :: alias
` invoke :: forwarded
` return invoke(forward, 23)
`}
module := try compile_owned(source, &diagnostic_store)
@@ -1684,7 +1684,7 @@ array_context_inference_empty_shapes_and_scalar_leaf_families test {
"main proc() string {\n a [2]mut string := [\"one\", \"two\"]\n b := a\n a[1] = \"three\"\n return b[1]\n}\n",
"plus proc(n int) int { return n + 1 }\nminus proc(n int) int { return n - 1 }\nmain proc() bool {\n a :: [plus, minus]\n return a[0](41) == 42 and a[1](43) == 42\n}\n",
"make proc() [2]i64 { return [20, 22] }\nmain proc() bool {\n return make()[0] + ([20, 22])[1] == 42\n}\n",
"count :: alias + 1\nalias :: 1\nmain proc() bool {\n a [count][0]bool :: [[], []]\n return true\n}\n",
"count :: forwarded + 1\nforwarded :: 1\nmain proc() bool {\n a [count][0]bool :: [[], []]\n return true\n}\n",
]
for (sources) |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
+9
View File
@@ -214,6 +214,15 @@ print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.
opening :: if (value.type == .array) "[" else "{"
closing :: if (value.type == .array) "]" else "}"
try io.write_all(output, opening)
if (shape.is_union) {
for (shape.fields) |field, index| {
if (!ir.union_active(module, value, index)) continue
if (field.type != .void) try print_value(output, module, ir.aggregate_item(module, value, index))
break
}
try io.write_all(output, closing)
return
}
for (0..count) |index| {
if (index != 0) try io.write_all(output, ", ")
try print_value(output, module, ir.aggregate_item(module, value, index))
+289 -13
View File
@@ -30,6 +30,7 @@ State :: struct {
next_token usize
paren_depth usize = 0
allow_record_literals bool = true
match_arm_body bool = false
}
#! borrows `tokens` and `source` while parsing; the returned Tree borrows `source` until ast.deinit.
@@ -156,7 +157,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
while true {
token :: try consume(state, .ident)
try arraylist.append(&names, token)
if (next_is(state, .ident) or next_is(state, .open_bracket) or next_is(state, .struct)) break
if (starts_type(current(state).kind)) break
_ = try consume(state, .comma)
}
@@ -237,6 +238,8 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
and (
next_n_is(state, 1, .ident)
or next_n_is(state, 1, .struct)
or next_n_is(state, 1, .enum) or next_n_is(state, 1, .union)
or next_n_is(state, 1, .distinct)
or (next_n_is(state, 1, .open_bracket) and array_starts_type(state))
or next_n_is(state, 1, .double_colon)
or next_n_is(state, 1, .colon_equal)
@@ -250,7 +253,8 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.for: return try parse_for(state, allow_else)
.return: return try parse_return(state, allow_else)
.break, .continue: return try parse_loop_control(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct: {
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct,
.enum, .union, .distinct, .alias, .dot, .match: {
expression :: try parse_expr(state)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
return try ast.add_expression_stmt(state.builder, expression)
@@ -264,6 +268,7 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .return)
if (
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
or (state.match_arm_body and next_is(state, .comma))
or (allow_else and next_is(state, .else))
) {
return try ast.add_return_stmt(state.builder, keyword.span, null)
@@ -272,6 +277,7 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
if (
!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)
and !(allow_else and next_is(state, .else))
and !(state.match_arm_body and next_is(state, .comma))
) {
return .unexpected_token
}
@@ -297,6 +303,7 @@ parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseErr
and !next_is(state, .close_curly)
and !next_is(state, .eof)
and !(allow_else and next_is(state, .else))
and !(state.match_arm_body and next_is(state, .comma))
) return .unexpected_token
if (keyword.kind == .break)
@@ -314,6 +321,7 @@ parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast.
and !next_is(state, .close_curly)
and !next_is(state, .eof)
and !(allow_else and next_is(state, .else))
and !(state.match_arm_body and next_is(state, .comma))
) {
return .unexpected_token
}
@@ -569,7 +577,7 @@ parse_arm proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
@hide:file
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
name :: try consume(state, .ident)
type_id ?ast.NodeId :: if (next_is(state, .ident) or next_is(state, .open_bracket) or next_is(state, .struct))
type_id ?ast.NodeId :: if (starts_type(current(state).kind))
try parse_type(state)
else
null
@@ -622,11 +630,14 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
)
}
if next_is(state, .struct) return try parse_record_type(state)
if next_is(state, .enum) return try parse_enum_type(state)
if next_is(state, .union) return try parse_union_type(state)
if (next_is(state, .distinct) or next_is(state, .alias)) return try parse_type_wrapper(state)
token :: try consume(state, .ident)
type := try ast.add_identifier(state.builder, token.span, token.data.str_id)
while next_is(state, .dot) {
state.next_token += 1
member :: try consume(state, .ident)
member :: try consume_member(state, false)
member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id)
type = try ast.add_selection(state.builder, ast.Span{
start = token.span.start,
@@ -637,6 +648,261 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
return type
}
@hide:file
starts_type proc(kind lexer.TokenKind) bool {
return (
kind == .ident or kind == .open_bracket or kind == .struct
or kind == .enum or kind == .union or kind == .distinct or kind == .alias
)
}
@hide:file
member_name proc(kind lexer.TokenKind) bool {
return match kind {
.ident, .if, .else, .for, .while, .proc, .return, .break, .continue,
.true, .false, .and, .or, .mut, .struct, .enum, .union, .distinct,
.alias, .match, .import, .test: true
else: false
}
}
@hide:file
consume_member proc(state @mut State, numeric bool) lexer.Token ! SyntaxError {
token :: current(state)
if (!member_name(token.kind) and !(numeric and token.kind == .int)) return .unexpected_token
state.next_token += 1
return token
}
@hide:file
parse_type_wrapper proc(state @mut State) ast.NodeId ! ParseError {
keyword :: current(state)
state.next_token += 1
operand :: try parse_type(state)
return try ast.add_type_operand(state.builder, ast.Span{
start = keyword.span.start,
end = ast.builder_span(state.builder, operand).end,
source = keyword.span.source,
}, operand, keyword.kind == .alias)
}
@hide:file
parse_enum_type proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .enum)
backing ?ast.NodeId := null
if next_is(state, .open_paren) {
state.next_token += 1
backing = try parse_type(state)
_ = try consume(state, .close_paren)
}
skip_newlines(state)
_ = try consume(state, .open_curly)
members arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&members)
skip_newlines(state)
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
if (
next_is(state, .ident)
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
) return try invalid_record(state, keyword.span.start, false)
start :: state.next_token
member :: parse_enum_member(state) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try unexpected_diagnostic(state)
span :: recover_record_field(state, start)
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
yield try ast.add_field(state.builder, span, null, null, invalid)
}
try arraylist.append(&members, member)
if next_is(state, .close_curly) break
if next_is(state, .comma) state.next_token += 1
else if (!next_is(state, .newline)) return try invalid_record(state, keyword.span.start, true)
skip_newlines(state)
}
if (!next_is(state, .close_curly)) return try invalid_record(state, keyword.span.start, false)
close :: try consume(state, .close_curly)
return try ast.add_enum_type(state.builder, ast.Span{
start = keyword.span.start, end = close.span.end, source = keyword.span.source,
}, backing, members.items)
}
@hide:file
parse_enum_member proc(state @mut State) ast.NodeId ! ParseError {
token :: try consume_member(state, false)
name :: try ast.add_identifier(state.builder, token.span, token.data.str_id)
value ?ast.NodeId := null
if next_is(state, .equal) {
state.next_token += 1
value = try parse_expr(state)
}
end :: if (value) |id| ast.builder_span(state.builder, id).end else token.span.end
return try ast.add_field(state.builder, ast.Span{
start = token.span.start,
end = end,
source = token.span.source,
}, name, null, value)
}
@hide:file
parse_union_type proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .union)
tag ?ast.NodeId := null
auto_tag := false
if next_is(state, .open_paren) {
state.next_token += 1
if next_is(state, .enum) {
state.next_token += 1
auto_tag = true
} else {
tag = try parse_type(state)
}
_ = try consume(state, .close_paren)
}
skip_newlines(state)
_ = try consume(state, .open_curly)
fields arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&fields)
skip_newlines(state)
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
if (
next_is(state, .ident)
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
) return try invalid_record(state, keyword.span.start, false)
start :: state.next_token
field :: parse_union_field(state, auto_tag or tag != null) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try unexpected_diagnostic(state)
span :: recover_record_field(state, start)
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
yield try ast.add_field(state.builder, span, null, invalid, null)
}
try arraylist.append(&fields, field)
if next_is(state, .close_curly) break
if next_is(state, .comma) state.next_token += 1
else if (!next_is(state, .newline)) return try invalid_record(state, keyword.span.start, true)
skip_newlines(state)
}
if (!next_is(state, .close_curly)) return try invalid_record(state, keyword.span.start, false)
close :: try consume(state, .close_curly)
return try ast.add_union_type(state.builder, ast.Span{
start = keyword.span.start, end = close.span.end, source = keyword.span.source,
}, tag, auto_tag, fields.items)
}
@hide:file
parse_union_field proc(state @mut State, tagged bool) ast.NodeId ! ParseError {
token :: if (tagged) try consume_member(state, false) else try consume(state, .ident)
name :: try ast.add_identifier(state.builder, token.span, token.data.str_id)
type :: try parse_type(state)
return try ast.add_field(state.builder, ast.Span{
start = token.span.start, end = ast.builder_span(state.builder, type).end, source = token.span.source,
}, name, type, null)
}
@hide:file
parse_match proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .match)
skip_newlines(state)
subject :: try parse_guard_expr(state)
skip_newlines(state)
_ = try consume(state, .open_curly)
previous_depth :: state.paren_depth
state.paren_depth = 0
defer state.paren_depth = previous_depth
arms arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&arms)
skip_newlines(state)
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
if (
next_is(state, .ident)
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
) return try invalid_record(state, keyword.span.start, false)
start :: state.next_token
arm :: parse_match_arm(state) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try unexpected_diagnostic(state)
span :: recover_record_field(state, start)
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
yield try ast.add_match_arm(state.builder, span, &[], null, invalid, true, false)
}
try arraylist.append(&arms, arm)
if next_is(state, .close_curly) break
if next_is(state, .comma) state.next_token += 1
else if (!next_is(state, .newline)) return try invalid_record(state, keyword.span.start, true)
skip_newlines(state)
}
if (!next_is(state, .close_curly)) return try invalid_record(state, keyword.span.start, false)
close :: try consume(state, .close_curly)
return try ast.add_match(state.builder, ast.Span{
start = keyword.span.start, end = close.span.end, source = keyword.span.source,
}, subject, arms.items)
}
@hide:file
parse_match_arm proc(state @mut State) ast.NodeId ! ParseError {
start :: current(state).span
patterns arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&patterns)
catch_all :: next_is(state, .else)
capture ?ast.NodeId := null
if (catch_all) state.next_token += 1
else {
while true {
try arraylist.append(&patterns, try parse_guard_expr(state))
if (!next_is(state, .comma)) break
state.next_token += 1
skip_newlines(state)
}
if next_is(state, .pipe) {
state.next_token += 1
# pointer payload captures are outside the current runtime representation.
if (next_is(state, .at) or next_is(state, .star)) return .unexpected_token
token :: try consume(state, .ident)
if (
token.span.end != token.span.start + 1
or state.builder.source[usize(token.span.start)] != '_'
) capture = try ast.add_identifier(state.builder, token.span, token.data.str_id)
_ = try consume(state, .pipe)
}
}
_ = try consume(state, .colon)
skip_newlines(state)
previous :: state.match_arm_body
state.match_arm_body = true
defer state.match_arm_body = previous
statement_body :: match current(state).kind {
.open_curly, .return, .break, .continue, .if, .while, .for: true
else: false
}
if (statement_body or named_binding_starts(state)) {
body :: try parse_arm(state, false)
return try ast.add_match_arm(state.builder, ast.Span{
start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source,
}, patterns.items, capture, body, false, catch_all)
}
value :: try parse_expr(state)
if next_is(state, .equal) {
statement :: try parse_assignment(state, value, false)
statements [1]ast.NodeId :: [statement]
body :: try ast.add_block(state.builder, ast.builder_span(state.builder, statement), statements[..])
return try ast.add_match_arm(state.builder, ast.Span{
start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source,
}, patterns.items, capture, body, false, catch_all)
}
return try ast.add_match_arm(state.builder, ast.Span{
start = start.start, end = ast.builder_span(state.builder, value).end, source = start.source,
}, patterns.items, capture, value, true, catch_all)
}
@hide:file
parse_record_type proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .struct)
@@ -711,15 +977,16 @@ parse_record_field proc(state @mut State, declaration bool) ast.NodeId ! ParseEr
name ?ast.NodeId := null
type ?ast.NodeId := null
value ?ast.NodeId := null
named :: next_is(state, .ident) and (
named :: member_name(current(state).kind) and (
(!declaration and next_n_is(state, 1, .equal))
or (declaration and (
next_n_is(state, 1, .ident) or next_n_is(state, 1, .open_bracket)
or next_n_is(state, 1, .struct)
or next_n_is(state, 1, .struct) or next_n_is(state, 1, .enum)
or next_n_is(state, 1, .union) or next_n_is(state, 1, .distinct)
))
)
if (named) {
token :: try consume(state, .ident)
token :: if (declaration) try consume(state, .ident) else try consume_member(state, false)
name = try ast.add_identifier(state.builder, token.span, token.data.str_id)
}
if (declaration) {
@@ -836,7 +1103,7 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
}
if (token.kind == .dot) {
state.next_token += 1
member :: try consume_either(state, &[.ident, .int])
member :: try consume_member(state, true)
member_id :: if (member.kind == .int)
try ast.add_literal(state.builder, .int, member.span)
else
@@ -999,7 +1266,7 @@ array_starts_type proc(state @State) bool {
if (depth == 0) {
if (i + 1 >= state.tokens.len) return false
next :: state.tokens[i + 1].kind
return next == .ident or next == .mut or next == .open_bracket or next == .struct
return starts_type(next) or next == .mut
}
}
if (kind == .open_curly or kind == .close_curly or kind == .eof) return false
@@ -1073,6 +1340,18 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
token :: current(state)
match token.kind {
.struct: return try parse_record_type(state)
.enum: return try parse_enum_type(state)
.union: return try parse_union_type(state)
.distinct, .alias: return try parse_type_wrapper(state)
.match: return try parse_match(state)
.dot: {
state.next_token += 1
member :: try consume_member(state, false)
member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id)
return try ast.add_enum_literal(state.builder, ast.Span{
start = token.span.start, end = member.span.end, source = token.span.source,
}, member_id)
}
.open_curly: {
if (state.allow_record_literals) return try parse_record_literal(state, null, token.span.start)
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
@@ -1126,10 +1405,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
return try ast.add_identifier(state.builder, token.span, token.data.str_id)
}
else: {
diagnostic :: if (token.data.diagnostic != diagnostics.no_diagnostic)
token.data.diagnostic
else
try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
diagnostic :: try unexpected_diagnostic(state)
if !is_expression_recovery_boundary(token.kind) {
state.next_token += 1
+124 -2
View File
@@ -940,7 +940,6 @@ recovers_missing_logical_operands_and_preserves_enclosing_nodes test {
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(6, diagnostics.count(&diagnostic_store))
try testing.expect_equal(2, ast.package_decl_count(&tree))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
try testing.expect_equal(5, ast.block_statement_count(&tree, function.body))
@@ -952,6 +951,11 @@ recovers_missing_logical_operands_and_preserves_enclosing_nodes test {
try testing.expect_equal(ast.BinaryOperator.logical_or, second_or.operator)
second_and :: ast.binary_expr(&tree, second_or.left)
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, second_and.right))
missing :: ast.invalid(&tree, second_and.right)
diagnostic :: diagnostics.get(&diagnostic_store, missing.diagnostic)?
try testing.expect_equal(diagnostics.Code.unexpected_token, diagnostic.code)
try testing.expect_equal(missing.span.start, diagnostic.span.start)
try testing.expect_equal(missing.span.end, diagnostic.span.end)
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, second_or.right))
third :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 2))
try testing.expect_equal(2, ast.call_arg_count(&tree, third.initializer))
@@ -1036,13 +1040,17 @@ rejects_control_tails_and_preserves_following_declarations test {
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(9, diagnostics.count(&diagnostic_store))
try testing.expect_equal(6, ast.package_decl_count(&tree))
for (usize(0)..usize(2)) |index| try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, index)))
for (usize(2)..usize(4)) |index| {
declaration :: ast.package_decl(&tree, index)
try testing.expect_equal(ast.DeclKind.statement, ast.decl_kind(&tree, declaration))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, ast.statement_decl(&tree, declaration).initializer))
invalid :: ast.invalid(&tree, ast.statement_decl(&tree, declaration).initializer)
diagnostic :: diagnostics.get(&diagnostic_store, invalid.diagnostic)?
try testing.expect_equal(diagnostics.Code.unexpected_token, diagnostic.code)
try testing.expect_equal(invalid.span.start, diagnostic.span.start)
try testing.expect_equal(invalid.span.end, diagnostic.span.end)
}
body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
try testing.expect_equal(2, ast.block_statement_count(&tree, body))
@@ -1588,3 +1596,117 @@ rolls_back_shared_nodes_extra_spans_and_diagnostics_on_allocation_failure test {
}
try testing.expect(failures > 0)
}
parses_enum_union_wrappers_and_keyword_variants test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`Tag :: enum(i32) { if = -2, else = 3 # preserve the member boundary
` return
`}
`Payload :: union(Tag) { if i32, else void, return struct { value i32 } }
`Auto :: union(enum) { value i32, empty void }
`Raw :: union { integer i32, real f64 }
`Id :: distinct i32
`Other :: alias Id
`value Payload :: Payload{ if = 4 }
`context Payload :: .return{value = 5}
`empty Payload :: .else
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
try testing.expect_equal(9, ast.package_decl_count(&tree))
enumeration :: ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer
try testing.expect(ast.enum_type(&tree, enumeration).backing != null)
try testing.expect_equal(3, ast.enum_member_count(&tree, enumeration))
member :: ast.field(&tree, ast.enum_member(&tree, enumeration, 0))
try testing.expect_equal("if", strpool.get_str(&strings, ast.name_id(&tree, member.name?))?)
try testing.expect_equal(ast.ExprKind.unary, ast.expr_kind(&tree, member.value?))
tagged :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)).initializer
automatic :: ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer
raw :: ast.statement_decl(&tree, ast.package_decl(&tree, 3)).initializer
try testing.expect(ast.union_type(&tree, tagged).tag != null)
try testing.expect(ast.union_type(&tree, automatic).auto_tag)
try testing.expect(!ast.union_type(&tree, raw).auto_tag)
try testing.expect_equal(2, ast.record_field_count(&tree, raw))
distinct_type :: ast.statement_decl(&tree, ast.package_decl(&tree, 4)).initializer
alias_value :: ast.statement_decl(&tree, ast.package_decl(&tree, 5)).initializer
try testing.expect_equal(ast.ExprKind.distinct_type, ast.expr_kind(&tree, distinct_type))
try testing.expect_equal(ast.ExprKind.alias_expr, ast.expr_kind(&tree, alias_value))
contextual :: ast.statement_decl(&tree, ast.package_decl(&tree, 7)).initializer
variant :: ast.record_literal(&tree, contextual).type?
try testing.expect_equal(ast.ExprKind.enum_literal, ast.expr_kind(&tree, variant))
try testing.expect_equal("return", strpool.get_str(&strings, ast.name_id(&tree, ast.enum_literal(&tree, variant)))?)
}
parses_match_patterns_payload_captures_and_statement_arms test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`choose proc(value int) int {
` return match value {
` 0, 1..=3: 10,
` 4: 20 # preserve the arm boundary
` else: 30
` }
`}
`use proc(value int) void {
` match value {
` .if, .return |payload|: { local :: payload }
` .else: return,
` else: {}
` }
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
choose :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
value_match :: ast.return_stmt(&tree, ast.block_statement(&tree, choose.body, 0)).value?
try testing.expect_equal(3, ast.match_arm_count(&tree, value_match))
first :: ast.match_arm(&tree, value_match, 0)
try testing.expect_equal(2, ast.match_pattern_count(&tree, first))
try testing.expect_equal(ast.ExprKind.range, ast.expr_kind(&tree, ast.match_pattern(&tree, first, 1)))
try testing.expect(ast.match_arm_view(&tree, first).expression)
use :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
statement_match :: ast.statement_expr(&tree, ast.block_statement(&tree, use.body, 0))
captured :: ast.match_arm_view(&tree, ast.match_arm(&tree, statement_match, 0))
try testing.expect(captured.capture != null)
try testing.expect(!captured.expression)
try testing.expect(ast.match_arm_view(&tree, ast.match_arm(&tree, statement_match, 2)).catch_all)
}
rejects_pointer_match_captures_and_preserves_siblings test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`broken :: enum { first
`after :: 1
`use proc(value int) void {
` match value {
` .item |@payload|: payload
` else: {}
` }
`}
`last :: 2
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
try testing.expect_equal(4, ast.package_decl_count(&tree))
try testing.expect_equal("after", ast.source_text(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 1)).name))
try testing.expect_equal("last", ast.source_text(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 3)).name))
}
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -69,8 +69,8 @@ propagates_numeric_context_before_defaulting_forward_and_mutable_aliases test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`forward float :: -alias * 2 + seed
`alias :: seed
`forward float :: -forwarded * 2 + seed
`forwarded :: seed
`seed := 3
`backward :: 4
`demand float :: backward
@@ -81,7 +81,7 @@ propagates_numeric_context_before_defaulting_forward_and_mutable_aliases test {
`text :: "honey"
`fixed int :: 7
`anchored :: fixed + 2
`nested float :: quotient / (alias + 1)
`nested float :: quotient / (forwarded + 1)
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
@@ -192,9 +192,9 @@ checks_forward_alias_calls_grouped_parameters_and_mutual_recursion test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`alias :: alpha
`forwarded :: alpha
`alpha proc(a, b int) void {
` (alias)(a, b)
` (forwarded)(a, b)
` beta(a + b, seed)
`}
`beta proc(total int, amount float) void {
@@ -203,7 +203,7 @@ checks_forward_alias_calls_grouped_parameters_and_mutual_recursion test {
`seed := 3
`main proc() void {
` beta(1, 2)
` (alias)(3, 4)
` (forwarded)(3, 4)
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
@@ -359,8 +359,8 @@ reports_nonvoid_fallthrough_without_poisoning_signatures_and_checks_unused_bodie
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`answer :: alias()
`alias :: make
`answer :: forwarded()
`forwarded :: make
`make proc() int {}
`unused proc() float {}
`broken proc() void {
@@ -762,13 +762,13 @@ checks_bool_comparisons_recursive_returns_and_nested_numeric_context test {
`flag bool :: true
`same :: flag == false
`different :: true != flag
`less :: alias < 4.0
`less :: forwarded < 4.0
`less_equal :: seed <= 5
`greater :: 6 > seed
`greater_equal :: seed >= 1
`equal :: seed == 3
`not_equal :: seed != 2
`alias :: seed
`forwarded :: seed
`seed := 3
`identity proc(value bool) bool { return value }
`factorial proc(n int) int {
@@ -1789,11 +1789,11 @@ range_checks_both_bounds_and_preserves_fixed_declarations_and_source_errors test
source ::
`real float :: 2
`flag :: true
`alias :: flag
`forwarded :: flag
`nothing proc() void {}
`main proc() void {
` for 1.5..2.5 |_| {}
` for alias.."text" |_| {}
` for forwarded.."text" |_| {}
` for nothing..nothing() |_| {}
` for real..3 |_| {}
` for absent_start..absent_end |_| {}
@@ -2206,8 +2206,8 @@ expect_array proc(result @Result, id ast.NodeId, len usize, mutable bool, elemen
fixed_arrays_contextualize_forward_aliases_counts_nested_literals_and_signatures test {
source ::
`context [2]f64 :: alias
`alias :: seed
`context [2]f64 :: forwarded
`forwarded :: seed
`seed := [1, 2]
`count :: later * 2 - 1
`later :: 2
+91 -1
View File
@@ -1,6 +1,7 @@
import "@std/debug"
import "@std/mem"
import "@std/hashmap"
import "@std/arraylist"
import "@source/strpool"
import "@source/ast"
@@ -99,6 +100,8 @@ State :: struct {
builtin_ids []strpool.StringId
allocator mem.Allocator
namespaces hashmap.AutoHashMap(ast.NodeId, Scope)
aliases []mut ast.NodeId
selections arraylist.ArrayList(ast.NodeId)
}
#! borrows `tree`, its identifier `pool`, and `diagnostic_store` during resolution.
@@ -138,6 +141,9 @@ resolve_packages proc(
errdefer mem.free(allocator, bindings)
mem.set(bindings, Binding{})
builtin_ids :: try intern_builtins(pool, &builtins)
aliases :: try mem.alloc(ast.NodeId, allocator, ast.node_count(tree))
defer mem.free(allocator, aliases)
mem.set(aliases, maxval!(ast.NodeId))
state := State{
tree = tree,
@@ -146,7 +152,10 @@ resolve_packages proc(
builtin_ids = builtin_ids[..],
allocator = allocator,
namespaces = hashmap.init(allocator),
aliases = aliases,
selections = arraylist.init(allocator),
}
defer arraylist.deinit(&state.selections)
defer {
for (state.namespaces.entries) |entry| {
if (entry.hash == 0) continue
@@ -184,6 +193,7 @@ resolve_packages proc(
}
for (imports) |imported| try resolve_import(&state, &file_scopes[usize(ast.node_span(tree, imported.node).source)], imported)
for (roots) |root| try resolve_package(&state, root, file_scopes)
try canonicalize_aliases(&state)
return Resolution{ storage = Storage{
bindings = bindings,
@@ -402,10 +412,62 @@ resolve_statement_decl proc(
id ast.NodeId,
) void ! (mem.AllocError | diagnostics.CapacityError) {
declaration :: ast.statement_decl(state.tree, id)
if (ast.expr_kind(state.tree, declaration.initializer) == .alias_expr) {
state.aliases[usize(id)] = ast.type_operand(state.tree, declaration.initializer)
}
if (declaration.type) |type| try resolve_expr(state, scope, type)
try resolve_expr(state, scope, declaration.initializer)
}
@hide:file
canonical_binding proc(state @State, original Binding) Binding {
value := original
for (0..(state.aliases.len)) |_| {
if (value.kind != .declaration) break
operand :: state.aliases[usize(value.data.declaration)]
if (operand == maxval!(ast.NodeId)) break
target :: state.bindings[usize(operand)]
if (target.kind == .none) break
if (target.kind == .declaration and target.data.declaration == value.data.declaration) break
value = target
}
return value
}
@hide:file
canonicalize_aliases proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) {
# aliases may forward-reference other aliases or imported namespace members.
for (0..(state.aliases.len)) |_| {
changed := false
for (state.bindings) |@entry| {
value :: canonical_binding(state, entry^)
if (value.kind != entry.kind) changed = true
else if (value.kind == .declaration and value.data.declaration != entry.data.declaration) changed = true
entry^ = value
}
for (state.selections.items) |id| {
expression :: ast.selection_expr(state.tree, id)
base :: state.bindings[usize(expression.base)]
if (base.kind != .namespace or state.bindings[usize(id)].kind != .none) continue
namespace :: hashmap.get(&state.namespaces, base.data.namespace)?
found :: if (ast.expr_kind(state.tree, expression.member) == .identifier)
hashmap.get(&namespace.names, ast.name_id(state.tree, expression.member))
else
null
value :: if (found) |member| canonical_binding(state, member) else Binding{
kind = .invalid,
data = BindingData{ diagnostic = try diagnostics.add(
state.diagnostics, .unknown_name, ast.node_span(state.tree, expression.member),
) },
}
state.bindings[usize(id)] = value
state.bindings[usize(expression.member)] = value
changed = true
}
if (!changed) break
}
}
@hide:file
resolve_invalid proc(state @mut State, id ast.NodeId) void {
if (state.bindings[usize(id)].kind != .none) return
@@ -426,6 +488,30 @@ resolve_expr proc(
tree :: state.tree
match ast.expr_kind(tree, id) {
.literal_int, .literal_float, .literal_bool, .literal_string: {}
.enum_literal: {}
.distinct_type, .alias_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
.enum_type: {
if (ast.enum_type(tree, id).backing) |backing| try resolve_expr(state, scope, backing)
for (0..ast.enum_member_count(tree, id)) |i| {
member :: ast.field(tree, ast.enum_member(tree, id, i))
if (member.value) |value| try resolve_expr(state, scope, value)
}
}
.match_expr: {
try resolve_expr(state, scope, ast.match_expr(tree, id).subject)
for (0..ast.match_arm_count(tree, id)) |i| {
arm_id :: ast.match_arm(tree, id, i)
arm :: ast.match_arm_view(tree, arm_id)
for (0..ast.match_pattern_count(tree, arm_id)) |j| {
try resolve_expr(state, scope, ast.match_pattern(tree, arm_id, j))
}
arm_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
defer hashmap.deinit(&arm_scope.names)
if (arm.capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture))
if (arm.expression) try resolve_expr(state, &arm_scope, arm.body)
else try resolve_block(state, &arm_scope, arm.body)
}
}
.identifier: {
span :: ast.node_span(tree, id)
name :: ast.name_id(tree, id)
@@ -441,6 +527,7 @@ resolve_expr proc(
}
.selection: {
expression :: ast.selection_expr(tree, id)
try arraylist.append(&state.selections, id)
try resolve_expr(state, scope, expression.base)
base :: state.bindings[usize(expression.base)]
value := Binding{}
@@ -494,10 +581,13 @@ resolve_expr proc(
try resolve_expr(state, scope, ast.array_element(tree, id, i))
}
}
.record_type, .record_literal: {
.record_type, .record_literal, .union_type: {
if (ast.expr_kind(tree, id) == .record_literal) {
if (ast.record_literal(tree, id).type) |type| try resolve_expr(state, scope, type)
}
if (ast.expr_kind(tree, id) == .union_type) {
if (ast.union_type(tree, id).tag) |tag| try resolve_expr(state, scope, tag)
}
for (0..ast.record_field_count(tree, id)) |i| {
field :: ast.field(tree, ast.record_field(tree, id, i))
if (field.type) |type| try resolve_expr(state, scope, type)
+123
View File
@@ -809,3 +809,126 @@ resolves_interleaved_compilations_with_independent_identifier_orders test {
try testing.expect_equal(usize(0), diagnostics.count(&first_diagnostics))
try testing.expect_equal(usize(0), diagnostics.count(&second_diagnostics))
}
resolves_match_capture_scopes_without_looking_up_member_names test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`Tag :: enum { if, else }
`Value :: union(Tag) { if i32, else void }
`inspect proc(value Value) void {
` match value {
` .if |payload|: { local :: payload }
` .else: { local :: 1 }
` }
` payload
` local
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
defer deinit(&resolution)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
matched :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0))
first :: ast.match_arm(&tree, matched, 0)
arm :: ast.match_arm_view(&tree, first)
local :: ast.statement_decl(&tree, ast.block_statement(&tree, arm.body, 0))
try testing.expect_equal(arm.capture?, binding(&resolution, local.initializer).data.declaration)
member :: ast.enum_literal(&tree, ast.match_pattern(&tree, first, 0))
try testing.expect_equal(BindingKind.none, binding(&resolution, member).kind)
for (usize(1)..usize(3)) |index| {
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, body, index))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, reference).kind)
}
}
canonicalizes_forward_function_global_and_builtin_aliases test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`invoke :: alias call
`copy :: alias storage
`Number :: alias i32
`call :: alias original
`original proc() i32 { return storage }
`storage i32 := 1
`main proc() Number {
` copy = invoke()
` return copy
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
defer deinit(&resolution)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
original :: ast.package_decl(&tree, 4)
storage :: ast.package_decl(&tree, 5)
main :: ast.function_decl(&tree, ast.package_decl(&tree, 6))
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, main.body, 0))
try testing.expect_equal(storage, binding(&resolution, assignment.target).data.declaration)
try testing.expect_equal(original, binding(&resolution, ast.call_expr(&tree, assignment.value).callee).data.declaration)
try testing.expect_equal(BindingKind.builtin, binding(&resolution, main.return_type).kind)
try testing.expect_equal(BuiltinType.i32, binding(&resolution, main.return_type).data.builtin)
try testing.expect_equal(original, binding(&resolution, ast.package_decl(&tree, 0)).data.declaration)
}
canonicalizes_imported_aliases_and_namespace_alias_selections test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
builder := ast.init_builder(mem.c_allocator, "")
defer ast.deinit_builder(&builder)
library_source :: "original proc() i32 { return storage }\nstorage i32 := 1\nexported :: alias original\n"
library_id :: try ast.set_source(&builder, "library.hon", library_source)
library_scan := lexer.init(mem.c_allocator)
defer lexer.deinit(&library_scan)
library_scan.source_id = library_id
try lexer.scan(&library_scan, library_source, &pool, &diagnostic_store)
library :: try parser.parse_source(&builder, library_scan.tokens.items, &diagnostic_store)
main_source ::
`lib :: import "library"
`invoke :: alias namespace.exported
`copy :: alias namespace.storage
`namespace :: alias lib
`main proc() i32 { copy = invoke()
` return copy
`}
main_id :: try ast.set_source(&builder, "main.hon", main_source)
main_scan := lexer.init(mem.c_allocator)
defer lexer.deinit(&main_scan)
main_scan.source_id = main_id
try lexer.scan(&main_scan, main_source, &pool, &diagnostic_store)
root :: try parser.parse_source(&builder, main_scan.tokens.items, &diagnostic_store)
tree := ast.finish(&builder, root)
defer ast.deinit(&tree)
roots [2]ast.NodeId :: [root, library]
imports [1]Import :: [Import{
node = ast.package_decl_at(&tree, root, 0),
name = strpool.get_id(&pool, "lib")?,
target = library,
}]
resolution := try resolve_packages(mem.c_allocator, &tree, roots[..], imports[..], &pool, &diagnostic_store)
defer deinit(&resolution)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
original :: ast.package_decl_at(&tree, library, 0)
storage :: ast.package_decl_at(&tree, library, 1)
main :: ast.function_decl(&tree, ast.package_decl_at(&tree, root, 4))
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, main.body, 0))
try testing.expect_equal(storage, binding(&resolution, assignment.target).data.declaration)
try testing.expect_equal(original, binding(&resolution, ast.call_expr(&tree, assignment.value).callee).data.declaration)
namespace :: ast.package_decl_at(&tree, root, 3)
try testing.expect_equal(BindingKind.namespace, binding(&resolution, namespace).kind)
}