feat: add records and tuples across execution paths

This commit is contained in:
2026-10-25 22:29:38 +01:00
parent 3e93c6f000
commit 280059d83d
18 changed files with 1408 additions and 198 deletions
+92 -2
View File
@@ -1,9 +1,9 @@
#! grammar:
#! * type ..... -> identifier | '[' (expr | '_') ']' ['mut'] type
#! * type ..... -> identifier {'.' identifier} | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' [field {(',' | newline) field}] '}'
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
#! * expr ..... -> identifier | literal | type | '[' [expr {',' expr} [',']] ']' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' identifier | expr ('..' | '..=') expr
#! * expr ..... -> identifier | literal | type | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' (identifier | int) | expr ('..' | '..=') expr
#! * decl ..... -> identifier [type] binding expr | ['test'] [identifier '::'] 'import' string
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
@@ -43,6 +43,8 @@ ExprKind :: enum {
range
array_type
array_literal
record_type
record_literal
index
selection
invalid
@@ -96,6 +98,9 @@ NodeKind :: enum {
expr_range
expr_array_type
expr_array_literal
expr_record_type
expr_record_literal
record_field
expr_index
expr_selection
stmt_expr
@@ -211,6 +216,18 @@ ArrayTypeView :: struct {
mutable bool
}
RecordLiteralView :: struct {
span Span
type ?NodeId
}
FieldView :: struct {
span Span
name ?NodeId
type ?NodeId
value ?NodeId
}
IndexView :: struct {
span Span
base NodeId
@@ -450,6 +467,44 @@ add_array_literal proc(builder @mut Builder, span Span, elements []NodeId) NodeI
}, span)
}
add_record_type proc(builder @mut Builder, span Span, fields []NodeId) NodeId ! mem.AllocError {
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_record_type,
data0 = NodeData{ extra = u32(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 {
start :: builder.storage.extra.items.len
for (fields) |field_id| _ = try add_extra(builder, u32(field_id))
raw_type :: if (type) |id| id else no_node
return try add_node(builder, Node{
kind = .expr_record_literal,
main = NodeData{ node_id = raw_type },
data0 = NodeData{ extra = u32(start) },
data1 = NodeData{ extra = u32(builder.storage.extra.items.len) },
}, span)
}
add_field proc(
builder @mut Builder, span Span, name, type, value ?NodeId,
) NodeId ! mem.AllocError {
raw_name :: if (name) |id| id else no_node
raw_type :: if (type) |id| id else no_node
raw_value :: if (value) |id| id else no_node
return try add_node(builder, Node{
kind = .record_field,
main = NodeData{ node_id = raw_name },
data0 = NodeData{ node_id = raw_type },
data1 = NodeData{ node_id = raw_value },
}, span)
}
add_index proc(builder @mut Builder, span Span, base, index NodeId) NodeId ! mem.AllocError {
return try add_node(builder, Node{
kind = .expr_index,
@@ -922,6 +977,8 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_range: .range
.expr_array_type: .array_type
.expr_array_literal: .array_literal
.expr_record_type: .record_type
.expr_record_literal: .record_literal
.expr_index: .index
.expr_selection: .selection
.invalid_expr: .invalid
@@ -1007,6 +1064,39 @@ array_element proc(tree @Tree, id NodeId, index usize) NodeId {
return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index])
}
record_literal proc(tree @Tree, id NodeId) RecordLiteralView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_record_literal)
return RecordLiteralView{
span = get_span(&tree.storage, id),
type = maybe_node_id(u32(node.main.node_id)),
}
}
field proc(tree @Tree, id NodeId) FieldView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .record_field)
return FieldView{
span = get_span(&tree.storage, id),
name = maybe_node_id(u32(node.main.node_id)),
type = maybe_node_id(u32(node.data0.node_id)),
value = maybe_node_id(u32(node.data1.node_id)),
}
}
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)
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(index < usize(node.data1.extra - node.data0.extra))
return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index])
}
index_expr proc(tree @Tree, id NodeId) IndexView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_index)
+14
View File
@@ -54,6 +54,13 @@ Code :: enum {
not_indexable
invalid_index_assignment
index_out_of_bounds
unknown_field
duplicate_field
missing_field
tuple_arity_mismatch
not_record
recursive_record
invalid_record_field_type
missing_entry
invalid_entry
literal_out_of_range
@@ -144,6 +151,13 @@ messages :: $enummap.init(Code, []u8, {
not_indexable = "indexing is currently implemented for fixed arrays only",
invalid_index_assignment = "indexed assignment requires a writable array element",
index_out_of_bounds = "index out of bounds",
unknown_field = "record has no such field",
duplicate_field = "record field is specified more than once",
missing_field = "record initializer is missing a required field",
tuple_arity_mismatch = "tuple initializer arity does not match its type",
not_record = "field selection requires a record or tuple",
recursive_record = "record has a recursive by-value layout",
invalid_record_field_type = "record fields require a concrete value type",
missing_entry = "program requires a main function",
invalid_entry = "main must be a function with no parameters",
literal_out_of_range = "numeric literal is outside its runtime type's range",
+36 -27
View File
@@ -11,7 +11,7 @@ Outcome :: struct {
}
#! release an escaped aggregate with its retained allocator; scalar and trap outcomes need no allocation.
#! strings and array descriptors borrow the module, which must outlive this outcome.
#! strings and aggregate descriptors borrow the module, which must outlive this outcome.
deinit proc(outcome @mut Outcome) void {
if (outcome.owned_elements) |elements| mem.free(outcome.allocator?, elements)
outcome.owned_elements = null
@@ -57,7 +57,7 @@ Frame :: struct {
}
#! executes typed IR with invocation/global storage released on every exit.
#! array results own flat storage released by `deinit`; string IDs and shapes resolve through `module`.
#! aggregate results own flat storage released by `deinit`; string IDs and shapes resolve through `module`.
run proc(
allocator mem.Allocator,
module @ir.Module,
@@ -75,7 +75,7 @@ run proc(
mem.set(globals, GlobalState{})
global_cells usize := 0
for (module.globals.items) |global| {
if (global.type != .array) continue
if (!ir.aggregate(global.type)) continue
cells :: module.arrays.items[usize(global.array_type)].cells
if (cells > maxval!(usize) - global_cells) return .storage_limit_reached
global_cells += cells
@@ -84,9 +84,9 @@ run proc(
defer mem.free(allocator, aggregate_globals)
global_offset usize := 0
for (module.globals.items) |global, i| {
if (global.type != .array) continue
if (!ir.aggregate(global.type)) continue
cells :: module.arrays.items[usize(global.array_type)].cells
globals[i].value = array_view(global.array_type, aggregate_globals[global_offset..global_offset + cells])
globals[i].value = aggregate_view(global.type, global.array_type, aggregate_globals[global_offset..global_offset + cells])
global_offset += cells
}
frames := arraylist.init(Frame, allocator)
@@ -125,7 +125,7 @@ run proc(
globals[global_index].status = .ready
}
if (frames.items.len == 1) {
if (value.type == .array) {
if (ir.aggregate(value.type)) {
escaped :: try mem.alloc(ir.Value, allocator, value.data.elements.len)
memcopy!(escaped, value.data.elements)
value.data.elements = escaped
@@ -204,13 +204,13 @@ run proc(
}
continue
}
.array_literal: {
.array_literal, .record_literal: {
destination :: &frames.items[frame_index].values[usize(instruction.destination)]
offset usize := 0
for (0..instruction.arguments_count) |i| {
operand :: block.arguments.items[instruction.arguments_start + i]
element :: frames.items[frame_index].values[usize(operand)]
if (element.type == .array) {
if (ir.aggregate(element.type)) {
count :: element.data.elements.len
memcopy!(destination.data.elements[offset..offset + count], element.data.elements)
offset += count
@@ -228,7 +228,11 @@ run proc(
if (index >= u64(shape.len)) {
return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span)
}
value = indexed_value(module, base.array_type, base.data.elements, usize(index))
value = ir.aggregate_item(module, base, usize(index))
}
.field: {
base :: frames.items[frame_index].values[usize(instruction.left)]
value = ir.aggregate_item(module, base, usize(instruction.slot))
}
.address_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
@@ -238,7 +242,7 @@ run proc(
cells = &[],
initialized = &local.initialized,
}
if (address.type == .array) address.cells = local.value.data.elements
if (ir.aggregate(address.type)) address.cells = local.value.data.elements
else address.scalar = &local.value
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{
@@ -264,7 +268,7 @@ run proc(
array_type = instruction.array_type,
cells = &[],
}
if (address.type == .array) address.cells = global.value.data.elements
if (ir.aggregate(address.type)) address.cells = global.value.data.elements
else address.scalar = &global.value
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{
@@ -286,7 +290,20 @@ run proc(
type = instruction.pointee, array_type = instruction.array_type,
cells = base.cells[start..start + width], initialized = base.initialized,
}
if (address.type != .array) address.scalar = &address.cells[0]
if (!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
address := Address{
type = instruction.pointee, array_type = instruction.array_type,
cells = base.cells[field.offset..field.offset + width], initialized = base.initialized,
}
if (!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) } }
}
@@ -300,10 +317,10 @@ run proc(
}
}
if (instruction.op == .load_address) {
value = if (address.type == .array) array_view(address.array_type, address.cells) else address.scalar?^
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 (address.type == .array) memcopy!(address.cells, source.data.elements)
if (ir.aggregate(address.type)) memcopy!(address.cells, source.data.elements)
else address.scalar?^ = source
continue
}
@@ -432,7 +449,7 @@ push_frame proc(
errdefer mem.free(allocator, addresses)
for (signature.array_storage.items) |storage| {
cells :: module.arrays.items[usize(storage.array_type)].cells
value :: array_view(storage.array_type, aggregate[storage.offset..storage.offset + cells])
value :: aggregate_view(module.arrays.items[usize(storage.array_type)].kind, storage.array_type, aggregate[storage.offset..storage.offset + cells])
if (storage.local) locals[usize(storage.index)].value = value
else values[usize(storage.index)] = value
}
@@ -496,13 +513,13 @@ checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
}
@hide:file
array_view proc(shape ir.ArrayId, cells []mut ir.Value) ir.Value {
return ir.Value{ type = .array, array_type = shape, data = ir.ValueData{ elements = cells } }
aggregate_view proc(type ir.Type, shape ir.ArrayId, cells []mut ir.Value) ir.Value {
return ir.Value{ type = type, array_type = shape, data = ir.ValueData{ elements = cells } }
}
@hide:file
copy_value proc(destination @mut ir.Value, source ir.Value) void {
if (source.type == .array) {
if (ir.aggregate(source.type)) {
# aggregate destinations already own disjoint fixed segments; never install an alias.
memcopy!(destination.data.elements, source.data.elements)
} else {
@@ -512,17 +529,9 @@ copy_value proc(destination @mut ir.Value, source ir.Value) void {
@hide:file
element_cells proc(module @ir.Module, shape ir.ArrayType) usize {
return if (shape.element == .array) module.arrays.items[usize(shape.element_array)].cells else 1
return if (ir.aggregate(shape.element)) module.arrays.items[usize(shape.element_array)].cells else 1
}
@hide:file
indexed_value proc(module @ir.Module, shape_id ir.ArrayId, cells []mut ir.Value, index usize) ir.Value {
shape :: module.arrays.items[usize(shape_id)]
if (shape.element != .array) return cells[index]
width :: element_cells(module, shape)
start :: index * width
return array_view(shape.element_array, cells[start..start + width])
}
@hide:file
checked_unsigned proc(op ir.Op, left u64, right u64) ?u64 {
+85 -24
View File
@@ -14,18 +14,22 @@ no_array :: maxval!(ArrayId)
no_value :: maxval!(ValueId)
Type :: enum {
invalid,
int,
float,
bool,
string,
void,
function,
array,
usize,
address,
i8, i16, i32, isize,
u8, u16, u32, u64,
invalid
int
float
bool
string
void
function
array
record
address
i8, i16, i32, isize
u8, u16, u32, u64, usize
}
aggregate proc(type Type) bool {
return type == .array or type == .record
}
#! integer storage widths for the supported 64-bit target; zero for noninteger types.
@@ -84,13 +88,22 @@ Value :: struct {
data ValueData = ValueData{ integer = 0 }
}
#! fixed recursive shape; add_array derives `cells`, the flattened scalar-leaf count.
RecordField :: struct {
type Type
array_type ArrayId = no_array
offset usize = 0
}
#! fixed recursive aggregate shape; constructors derive the flattened scalar-leaf count.
#! record fields are owned by the module; array metadata retains its existing representation.
ArrayType :: struct {
element Type
kind Type = .array
element Type = .invalid
element_array ArrayId = no_array
len usize
cells usize
mutable bool
len usize = 0
cells usize = 0
mutable bool = false
fields []mut RecordField = &[]
}
#! maps a local slot or temporary to its preallocated, by-value frame segment.
@@ -109,10 +122,13 @@ Op :: enum {
reset_local
load_global
array_literal
record_literal
field
index
address_local
address_global
index_address
field_address
load_address
store_address
negate
@@ -244,8 +260,8 @@ add_array proc(
len usize,
mutable bool,
) ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
debug.assert((element == .array) == (element_array != no_array))
width :: if (element == .array) module.arrays.items[usize(element_array)].cells else 1
debug.assert(aggregate(element) == (element_array != no_array))
width :: if (aggregate(element)) module.arrays.items[usize(element_array)].cells else 1
if (width != 0 and len > divtrunc!(maxval!(usize), width)) return .storage_limit_reached
cells :: len * width
if (cells > divtrunc!(maxval!(usize), sizeof!(Value))) return .storage_limit_reached
@@ -261,6 +277,48 @@ add_array proc(
return id
}
#! copies declaration-order fields and derives their flattened offsets; child shapes must exist.
add_record proc(
module @mut Module,
fields []RecordField,
) 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)
cells usize := 0
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 }
cells += width
}
id :: ArrayId(module.arrays.items.len)
try arraylist.append(&module.arrays, ArrayType{ kind = .record, fields = owned, cells = cells })
return id
}
#! returns a borrowed child value from flattened aggregate storage, preserving zero-cell shapes.
aggregate_item proc(module @Module, value Value, index usize) Value {
shape :: module.arrays.items[usize(value.array_type)]
type := shape.element
child := shape.element_array
start usize := 0
width usize := 1
if (shape.kind == .record) {
field :: shape.fields[index]
type = field.type
child = field.array_type
start = field.offset
} 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] } }
}
@hide:file
prepare_storage proc(module @Module, function @mut Function, index u32, shape ArrayId, local bool) void ! (mem.AllocError | diagnostics.CapacityError) {
cells :: module.arrays.items[usize(shape)].cells
@@ -281,13 +339,13 @@ add_local proc(
type Type,
shape ArrayId,
) LocalId ! (mem.AllocError | diagnostics.CapacityError) {
debug.assert((type == .array) == (shape != no_array))
debug.assert(aggregate(type) == (shape != no_array))
body :: &module.functions.items[usize(function)]
if (body.locals.items.len >= usize(maxval!(LocalId))) return .storage_limit_reached
id :: LocalId(body.locals.items.len)
try arraylist.append(&body.locals, type)
errdefer _ = arraylist.pop(&body.locals)
if (type == .array) try prepare_storage(module, body, u32(id), shape, true)
if (aggregate(type)) try prepare_storage(module, body, u32(id), shape, true)
return id
}
@@ -305,9 +363,9 @@ add_parameter proc(
return try add_local(module, function, type, shape)
}
#! appends an instruction and derives its result count, array backing, and address presence.
#! appends an instruction and derives its result count, aggregate backing, and address presence.
#! no_value requests a fresh result; explicit result IDs support non-SSA definitions across blocks.
#! repeated array results must retain their shape and reuse their existing segment.
#! repeated aggregate results must retain their shape and reuse their existing segment.
#! failures leave instruction and storage lengths, totals, counts, and flags unchanged.
emit proc(
module @mut Module,
@@ -336,7 +394,7 @@ emit proc(
}
try arraylist.append(&target.instructions, operation)
errdefer _ = arraylist.pop(&target.instructions)
if (produces_value and operation.type == .array) {
if (produces_value and aggregate(operation.type)) {
debug.assert(operation.array_type != no_array)
registered := false
if (usize(operation.destination) < body.value_count) {
@@ -377,6 +435,9 @@ deinit proc(module @mut Module) void {
for (module.strings.items) |string|
mem.free(module.allocator, string.allocation)
for (module.arrays.items) |shape| {
if (shape.kind == .record) mem.free(module.allocator, shape.fields)
}
arraylist.deinit(&module.functions)
arraylist.deinit(&module.globals)
arraylist.deinit(&module.strings)
+9 -3
View File
@@ -33,6 +33,7 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "and", .and },
{ "or", .or },
{ "mut", .mut },
{ "struct", .struct },
{ "import", .import },
{ "test", .test },
])
@@ -102,7 +103,12 @@ scan proc(
if is_digit(char) {
start :: cursor
result :: scan_number(start, program) catch |err| {
member :: (
state.tokens.items.len > 0
and state.tokens.items[state.tokens.items.len - 1].kind == .dot
and usize(state.tokens.items[state.tokens.items.len - 1].span.end) == start
)
result :: scan_number(start, program, !member) catch |err| {
id :: try diagnostics.add(
diagnostic_store,
diagnostic_code(err.code),
@@ -362,12 +368,12 @@ ScanNumResult :: struct {
}
@hide:file
scan_number proc(start usize, program []u8) ScanNumResult ! ScanError {
scan_number proc(start usize, program []u8, allow_decimal bool) ScanNumResult ! ScanError {
cursor := start
has_decimal := false
while (cursor < program.len and is_digit(program[cursor])) cursor += 1
if (
cursor < program.len and program[cursor] == '.'
allow_decimal and cursor < program.len and program[cursor] == '.'
and !(cursor + 1 < program.len and program[cursor + 1] == '.')
) {
has_decimal = true
+1
View File
@@ -25,6 +25,7 @@ TokenKind :: enum {
true, false
and, or
mut
struct
import, test
# literals
+24 -2
View File
@@ -43,19 +43,41 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
try write_type(output, state.module, type)
try io.print(", ptr %r{}\n", output, {pointer})
}
.array_literal: {
.array_literal, .record_literal: {
for (0..instruction.arguments_count) |index| {
operand :: block.arguments.items[instruction.arguments_start + index]
value :: try load(state, operand)
pointer :: fresh(state)
try io.print(" %r{} = getelementptr ", output, {pointer})
try write_type(output, state.module, type)
try io.print(", ptr %v{}, i64 0, i64 {}\n store ", output, {u32(instruction.destination), index})
index_type []u8 :: if (instruction.op == .record_literal) "i32" else "i64"
try io.print(", ptr %v{}, i64 0, {s} {}\n store ", output, {u32(instruction.destination), index_type, index})
try write_type(output, state.module, state.values[usize(operand)])
try io.print(" %r{}, ptr %r{}\n", output, {value, pointer})
}
return
}
.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})
}
.field_address: {
base_type :: state.values[usize(instruction.left)]
base :: try load(state, instruction.left)
data :: fresh(state)
flag :: fresh(state)
pointer :: fresh(state)
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})
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})
}
.index, .index_address: {
index :: try load(state, instruction.right)
base_type :: state.values[usize(instruction.left)]
+58 -20
View File
@@ -72,7 +72,10 @@ emit proc(
try io.print(" zeroinitializer\n@gs{} = internal global i8 0\n", output, {index})
try emit_global(output, module, global, index)
}
for (module.arrays.items) |_, index| try emit_array_printer(output, module, ir.ArrayId(index))
for (module.arrays.items) |shape, index| {
if (shape.kind == .record) try emit_record_printer(output, module, ir.ArrayId(index))
else try emit_array_printer(output, module, ir.ArrayId(index))
}
for (module.functions.items) |function, index| {
values :: try mem.alloc(Type, allocator, function.value_count)
defer mem.free(allocator, values)
@@ -91,13 +94,13 @@ emit proc(
if (module.entry) |entry| {
function :: module.functions.items[usize(entry)]
result :: Type{ kind = function.result, shape = function.result_array_type }
if (result.kind != .void) try io.write_all(output, " %result = ")
if (result.kind != .void) try io.write_all(output, " %r0 = ")
else try io.write_all(output, " ")
try io.write_all(output, "call ")
try write_type(output, module, result)
try io.print(" @f{}()\n", output, {u32(entry)})
if (result.kind != .void and result.kind != .invalid) {
try print_call(output, module, result, "%result")
try print_call(output, module, result, 0)
try io.write_all(output, " call void @out(i32 1, ptr @newline, i64 1)\n")
}
try io.write_all(output, " call void @report(i64 -1)\n")
@@ -138,6 +141,15 @@ write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteE
try write_type(output, module, Type{ kind = shape.element, shape = shape.element_array })
try io.write_all(output, "]")
}
.record: {
shape :: module.arrays.items[usize(type.shape)]
try io.write_all(output, "{ ")
for (shape.fields) |field, index| {
if (index != 0) try io.write_all(output, ", ")
try write_type(output, module, Type{ kind = field.type, shape = field.array_type })
}
try io.write_all(output, " }")
}
.void: try io.write_all(output, "void")
# invalid storage only belongs to recovery paths that trap before reading it.
.invalid: try io.write_all(output, "i8")
@@ -148,7 +160,7 @@ write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteE
local_type proc(function @ir.Function, index usize) Type {
type := Type{ kind = function.locals.items[index] }
if (type.kind == .void) type.kind = .invalid
if (type.kind == .array) {
if (ir.aggregate(type.kind)) {
for (function.array_storage.items) |storage| {
if (storage.local and usize(storage.index) == index) {
type.shape = storage.array_type
@@ -244,24 +256,24 @@ emit_global proc(output io.Writer, module @ir.Module, global ir.Global, index us
}
@hide:file
print_call proc(output io.Writer, module @ir.Module, type Type, operand []u8) void ! io.WriteError {
print_call proc(output io.Writer, module @ir.Module, type Type, operand usize) void ! io.WriteError {
match type.kind {
.i8, .i16, .i32, .int, .isize, .u8, .u16, .u32, .u64, .usize: {
bits :: ir.integer_bits(type.kind)
unsigned :: ir.unsigned_integer(type.kind)
if (bits < 64) {
extension []u8 :: if (unsigned) "zext" else "sext"
try io.print(" %print_value = {s} i{} {s} to i64\n", output, {extension, bits, operand})
try io.print(" call void @print_integer(i32 1, i64 %print_value, i1 {})\n", output, {unsigned})
} else try io.print(" call void @print_integer(i32 1, i64 {s}, i1 {})\n", output, {operand, unsigned})
try io.print(" %print_value{} = {s} i{} %r{} to i64\n", output, {operand, extension, bits, operand})
try io.print(" call void @print_integer(i32 1, i64 %print_value{}, i1 {})\n", output, {operand, unsigned})
} else try io.print(" call void @print_integer(i32 1, i64 %r{}, i1 {})\n", output, {operand, unsigned})
}
.float: try io.print(" call void @print_float(double {s})\n", output, {operand})
.bool: try io.print(" call void @print_bool(i1 {s})\n", output, {operand})
.string: try io.print(" call void @print_string(%string {s})\n", output, {operand})
.array: {
.float: try io.print(" call void @print_float(double %r{})\n", output, {operand})
.bool: try io.print(" call void @print_bool(i1 %r{})\n", output, {operand})
.string: try io.print(" call void @print_string(%string %r{})\n", output, {operand})
.array, .record: {
try io.print(" call void @print_array{}(", output, {u32(type.shape)})
try write_type(output, module, type)
try io.print(" {s})\n", output, {operand})
try io.print(" %r{})\n", output, {operand})
}
else: unreachable
}
@@ -282,15 +294,33 @@ emit_array_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void
try io.print(" %done = icmp eq i64 %index, {}\n", output, {shape.len})
try io.write_all(output, " br i1 %done, label %end, label %separator\nseparator:\n %first = icmp eq i64 %index, 0\n br i1 %first, label %element, label %comma\ncomma:\n call void @out(i32 1, ptr @separator, i64 2)\n br label %element\nelement:\n %pointer = getelementptr ")
try write_type(output, module, type)
try io.write_all(output, ", ptr %storage, i64 0, i64 %index\n %item = load ")
try io.write_all(output, ", ptr %storage, i64 0, i64 %index\n %r0 = load ")
try write_type(output, module, element)
try io.write_all(output, ", ptr %pointer\n")
# functions may occur inside arrays without being printable source results.
if (element.kind != .function) try print_call(output, module, element, "%item")
if (element.kind != .function) try print_call(output, module, element, 0)
else try io.write_all(output, " call void @exit(i32 2)\n")
try io.write_all(output, " %next = add i64 %index, 1\n br label %loop\nend:\n call void @out(i32 1, ptr @close, i64 1)\n ret void\n}\n")
}
@hide:file
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 }
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")
for (shape.fields) |field, index| {
if (index != 0) try io.write_all(output, " call void @out(i32 1, ptr @separator, i64 2)\n")
try io.print(" %r{} = extractvalue ", output, {index})
try write_type(output, module, type)
try io.print(" %value, {}\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, " 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
@@ -356,20 +386,28 @@ write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void !
.function: try io.print("@f{}", output, {u32(value.data.function)})
.array: {
shape :: module.arrays.items[usize(value.array_type)]
width :: if (shape.element == .array) module.arrays.items[usize(shape.element_array)].cells else 1
try io.write_all(output, "[")
for (0..shape.len) |index| {
if (index != 0) try io.write_all(output, ", ")
element := ir.Value{}
if (shape.element == .array) {
element = ir.Value{ type = .array, array_type = shape.element_array, data = ir.ValueData{ elements = value.data.elements[index * width..(index + 1) * width] } }
} else element = value.data.elements[index]
element :: ir.aggregate_item(module, value, index)
try write_type(output, module, Type{ kind = element.type, shape = element.array_type })
try io.write_all(output, " ")
try write_constant(output, module, element)
}
try io.write_all(output, "]")
}
.record: {
shape :: module.arrays.items[usize(value.array_type)]
try io.write_all(output, "{ ")
for (shape.fields) |_, index| {
if (index != 0) try io.write_all(output, ", ")
field :: ir.aggregate_item(module, value, index)
try write_type(output, module, Type{ kind = field.type, shape = field.array_type })
try io.write_all(output, " ")
try write_constant(output, module, field)
}
try io.write_all(output, " }")
}
else: unreachable
}
}
+2
View File
@@ -43,6 +43,8 @@ runtime ::
`@newline = private constant [1 x i8] c"\0A"
`@open = private constant [1 x i8] c"["
`@close = private constant [1 x i8] c"]"
`@record_open = private constant [1 x i8] c"{"
`@record_close = private constant [1 x i8] c"}"
`@separator = private constant [2 x i8] c", "
`@write_error = private constant [44 x i8] c": infrastructure error: cannot write output\0A"
`@read = private constant [42 x i8] c"local value was read before initialization"
+126 -5
View File
@@ -33,6 +33,7 @@ State :: struct {
storage []mut Storage
arrays []mut ir.ArrayId
arguments arraylist.ArrayList(ir.ValueId)
records []mut ir.ArrayId
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
@@ -75,6 +76,9 @@ lower_package proc(
arrays :: try mem.alloc(ir.ArrayId, allocator, typecheck.array_type_count(checked))
defer mem.free(allocator, arrays)
mem.set(arrays, ir.no_array)
records :: try mem.alloc(ir.ArrayId, allocator, typecheck.record_type_count(checked))
defer mem.free(allocator, records)
mem.set(records, ir.no_array)
state := State{
tree = tree,
resolution = resolution,
@@ -83,6 +87,7 @@ lower_package proc(
module = &module,
storage = storage,
arrays = arrays,
records = records,
arguments = arraylist.init(ir.ValueId, allocator),
}
defer arraylist.deinit(&state.arguments)
@@ -96,6 +101,7 @@ lower_package proc(
storage[usize(id)] = Storage{ kind = .function, index = u32(function) }
}
.statement: {
if (typecheck.type_of(checked, id).kind == .type_value) continue
initializer :: try add_function(&module, ast.node_span(tree, id))
global :: u32(module.globals.items.len)
try arraylist.append(&module.globals, ir.Global{
@@ -114,7 +120,9 @@ lower_package proc(
id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) {
.function: try lower_function(&state, id)
.statement: try lower_global(&state, id)
.statement: {
if (typecheck.type_of(checked, id).kind != .type_value) try lower_global(&state, id)
}
.import, .invalid: {}
}
}
@@ -135,17 +143,19 @@ lower_type proc(type typecheck.Type) ir.Type {
.u32: .u32
.u64: .u64
.array: .array
.record: .record
.float: .float
.bool: .bool
.string: .string
.void: .void
.function: .function
.none, .invalid, .range: .invalid
.none, .invalid, .range, .type_value: .invalid
}
}
@hide:file
array_shape proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (type.kind == .record) return try prepare_record(state, type.record_type)
if (type.kind != .array) return ir.no_array
# checker history includes unresolved shapes; materialize only final used types.
return try prepare_array(state, type.array_type)
@@ -156,7 +166,7 @@ prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.All
if (state.arrays[usize(id)] != ir.no_array) return state.arrays[usize(id)]
descriptor :: typecheck.array_type(state.checked, id)
child := ir.no_array
if (descriptor.element.kind == .array) child = try prepare_array(state, descriptor.element.array_type)
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),
@@ -168,6 +178,24 @@ prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.All
return result
}
@hide:file
prepare_record proc(state @mut State, id typecheck.RecordId) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (state.records[usize(id)] != ir.no_array) return state.records[usize(id)]
descriptor :: typecheck.record_type(state.checked, id)
fields := arraylist.init(ir.RecordField, state.module.allocator)
defer arraylist.deinit(&fields)
try arraylist.reserve(&fields, descriptor.fields.len)
for (descriptor.fields) |field| {
try arraylist.append(&fields, ir.RecordField{
type = lower_type(field.type),
array_type = try array_shape(state, field.type),
})
}
shape :: try ir.add_record(state.module, fields.items)
state.records[usize(id)] = shape
return shape
}
@hide:file
add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError {
id :: ir.FunctionId(module.functions.items.len)
@@ -315,6 +343,7 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro
statement :: ast.block_statement(state.tree, block, i)
match ast.stmt_kind(state.tree, statement) {
.declaration: {
if (typecheck.type_of(state.checked, statement).kind == .type_value) continue
type :: typecheck.type_of(state.checked, statement)
local :: try ir.add_local(
state.module,
@@ -355,6 +384,7 @@ lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (m
for (0..ast.block_statement_count(state.tree, block)) |i| {
declaration :: ast.block_statement(state.tree, block, i)
if (ast.stmt_kind(state.tree, declaration) != .declaration) continue
if (typecheck.type_of(state.checked, declaration).kind == .type_value) continue
_ = try emit(state, ir.Instruction{
op = .reset_local,
slot = state.storage[usize(declaration)].index,
@@ -539,7 +569,9 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(tree, id)
if (ast.expr_kind(tree, assignment.target) == .index) {
if (ast.expr_kind(tree, assignment.target) == .index
or (ast.expr_kind(tree, assignment.target) == .selection
and sema.binding(state.resolution, assignment.target).kind == .none)) {
value :: try lower_expression(state, assignment.value)
if (state.terminated) return
if (!is_place(state, assignment.target)) {
@@ -577,6 +609,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 (trap_binding(state, id)) return
declaration :: ast.statement_decl(tree, id)
if (declaration.type) |annotation| if (trap_type(state, annotation)) return
@@ -642,10 +675,66 @@ lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError
return try emit(state, ir.Instruction{ op = .load_local, type = .bool, slot = slot, span = span })
}
@hide:file
lower_record proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
explicit_count :: ast.record_field_count(tree, id)
try arraylist.reserve(&state.arguments, scratch_start + explicit_count)
for (0..explicit_count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..explicit_count) |i| {
value :: try lower_expression(state, ast.field(tree, ast.record_field(tree, id, i)).value?)
if (state.terminated) return ir.no_value
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)
for (descriptor.fields) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..(descriptor.fields.len)) |i| {
initializer :: typecheck.initializer_field(state.checked, id, i)
value := ir.no_value
# ponytail: quadratic field reorder; retain an inverse map if large records make lowering costly.
for (0..explicit_count) |j| {
if (ast.field(tree, ast.record_field(tree, id, j)).value? == initializer) {
value = state.arguments.items[scratch_start + j]
break
}
}
if (value == ir.no_value) value = try lower_expression(state, initializer)
if (state.terminated) return ir.no_value
state.arguments.items[ordered_start + i] = value
}
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.reserve(&block.arguments, start + descriptor.fields.len)
for (0..(descriptor.fields.len)) |i| try arraylist.append(&block.arguments, state.arguments.items[ordered_start + i])
return try emit(state, ir.Instruction{
op = .record_literal,
type = .record,
array_type = try array_shape(state, type),
arguments_start = start,
arguments_count = descriptor.fields.len,
span = ast.node_span(tree, id),
})
}
@hide:file
is_place proc(state @State, id ast.NodeId) bool {
match ast.expr_kind(state.tree, id) {
.index: return is_place(state, ast.index_expr(state.tree, id).base)
.selection: {
if (sema.binding(state.resolution, id).kind == .none) {
return is_place(state, ast.selection_expr(state.tree, id).base)
}
binding :: sema.binding(state.resolution, id)
if (binding.kind != .declaration) return false
location :: state.storage[usize(binding.data.declaration)]
return location.kind == .local or location.kind == .global
}
.identifier: {
binding :: sema.binding(state.resolution, id)
if (binding.kind == .invalid) return true
@@ -661,6 +750,21 @@ is_place proc(state @State, id ast.NodeId) bool {
lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id)
checked :: typecheck.type_of(state.checked, id)
if (ast.expr_kind(state.tree, id) == .selection
and sema.binding(state.resolution, id).kind == .none) {
expression :: ast.selection_expr(state.tree, id)
base :: try lower_place(state, expression.base)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .field_address,
type = .address,
pointee = lower_type(checked),
array_type = try array_shape(state, checked),
left = base,
slot = u32(typecheck.field_index(state.checked, id)),
span = span,
})
}
if (ast.expr_kind(state.tree, id) == .index) {
expression :: ast.index_expr(state.tree, id)
base :: try lower_place(state, expression.base)
@@ -706,11 +810,12 @@ 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: {
.array_type, .record_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)
.array_literal: {
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
@@ -769,6 +874,22 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
return try lower_string(state, id)
}
.identifier, .selection: {
if (ast.expr_kind(tree, id) == .selection
and sema.binding(state.resolution, id).kind == .none) {
if (is_place(state, id)) {
address :: try lower_place(state, id)
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .load_address, type = type, array_type = shape, left = address, span = span,
})
}
base :: try lower_expression(state, ast.selection_expr(tree, id).base)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{
op = .field, type = type, array_type = shape, left = base,
slot = u32(typecheck.field_index(state.checked, id)), span = span,
})
}
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
binding :: sema.binding(state.resolution, id)
location :: state.storage[usize(binding.data.declaration)]
+9 -21
View File
@@ -208,29 +208,17 @@ print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.
.float: try io.print("{d}", output, {value.data.real})
.bool: try io.print("{}", output, {value.data.boolean})
.string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)})
.array: {
.array, .record: {
shape :: module.arrays.items[usize(value.array_type)]
stride usize :: if (shape.element == .array)
module.arrays.items[usize(shape.element_array)].cells
else
1
try io.print("[", output, {})
for 0..(shape.len) |index| {
if (index != 0) try io.print(", ", output, {})
element := ir.Value{}
if shape.element == .array {
start :: index * stride
element = ir.Value{
type = .array,
array_type = shape.element_array,
data = ir.ValueData{ elements = value.data.elements[start..(start + stride)] },
}
} else {
element = value.data.elements[index]
}
try print_value(output, module, element)
count :: if (value.type == .array) shape.len else shape.fields.len
opening :: if (value.type == .array) "[" else "{"
closing :: if (value.type == .array) "]" else "}"
try io.write_all(output, opening)
for (0..count) |index| {
if (index != 0) try io.write_all(output, ", ")
try print_value(output, module, ir.aggregate_item(module, value, index))
}
try io.print("]", output, {})
try io.write_all(output, closing)
}
else: unreachable
}
+220 -10
View File
@@ -29,6 +29,7 @@ State :: struct {
allocator mem.Allocator
next_token usize
paren_depth usize = 0
allow_record_literals bool = true
}
#! borrows `tokens` and `source` while parsing; the returned Tree borrows `source` until ast.deinit.
@@ -155,7 +156,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)) break
if (next_is(state, .ident) or next_is(state, .open_bracket) or next_is(state, .struct)) break
_ = try consume(state, .comma)
}
@@ -235,6 +236,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
next_is(state, .ident)
and (
next_n_is(state, 1, .ident)
or next_n_is(state, 1, .struct)
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)
@@ -248,7 +250,7 @@ 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: {
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct: {
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)
@@ -424,7 +426,7 @@ parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .for)
skip_newlines(state)
iterable_start :: state.next_token
iterable := try parse_expr(state)
iterable := try parse_guard_expr(state)
grouped :: parenthesized_guard(state, iterable_start)
skip_newlines(state)
captures :: parse_captures(state) catch |err| {
@@ -478,10 +480,18 @@ parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
)
}
@hide:file
parse_guard_expr proc(state @mut State) ast.NodeId ! ParseError {
previous :: state.allow_record_literals
state.allow_record_literals = false
defer state.allow_record_literals = previous
return try parse_expr(state)
}
@hide:file
parse_guard proc(state @mut State) ast.NodeId ! ParseError {
condition_start :: state.next_token
condition := parse_expr(state) catch |err| {
condition := parse_guard_expr(state) catch |err| {
match err {
.unexpected_token: {}
else: return err
@@ -559,7 +569,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))
type_id ?ast.NodeId :: if (next_is(state, .ident) or next_is(state, .open_bracket) or next_is(state, .struct))
try parse_type(state)
else
null
@@ -611,8 +621,169 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
count, element, mutable,
)
}
if next_is(state, .struct) return try parse_record_type(state)
token :: try consume(state, .ident)
return try ast.add_identifier(state.builder, token.span, token.data.str_id)
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_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,
end = member.span.end,
source = token.span.source,
}, type, member_id)
}
return type
}
@hide:file
parse_record_type proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .struct)
return try parse_record(state, null, keyword.span.start, true)
}
@hide:file
parse_record_literal proc(
state @mut State, type ?ast.NodeId, start u32,
) ast.NodeId ! ParseError {
return try parse_record(state, type, start, false)
}
@hide:file
parse_record proc(
state @mut State, type ?ast.NodeId, start u32, declaration bool,
) ast.NodeId ! ParseError {
open :: try consume(state, .open_curly)
previous :: state.allow_record_literals
state.allow_record_literals = true
defer state.allow_record_literals = previous
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)) {
# leave the next declaration intact when a record's closing brace is missing.
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, start, false)
field_start :: state.next_token
field :: parse_record_field(state, declaration) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try unexpected_diagnostic(state)
span :: recover_record_field(state, field_start)
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
recovered_type ?ast.NodeId :: if (declaration) invalid else null
recovered_value ?ast.NodeId :: if (declaration) null else invalid
yield try ast.add_field(state.builder, span, null, recovered_type, recovered_value)
}
try arraylist.append(&fields, field)
if next_is(state, .close_curly) break
if next_is(state, .comma) {
state.next_token += 1
skip_newlines(state)
continue
}
if (declaration and next_is(state, .newline)) {
skip_newlines(state)
continue
}
after_field :: state.next_token
skip_newlines(state)
if next_is(state, .close_curly) break
state.next_token = after_field
return try invalid_record(state, start, true)
}
if (!next_is(state, .close_curly)) return try invalid_record(state, start, false)
close :: try consume(state, .close_curly)
span :: ast.Span{ start = start, end = close.span.end, source = open.span.source }
if (declaration) return try ast.add_record_type(state.builder, span, fields.items)
return try ast.add_record_literal(state.builder, span, type, fields.items)
}
@hide:file
parse_record_field proc(state @mut State, declaration bool) ast.NodeId ! ParseError {
start :: current(state).span
start_token :: state.next_token
name ?ast.NodeId := null
type ?ast.NodeId := null
value ?ast.NodeId := null
named :: next_is(state, .ident) 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)
))
)
if (named) {
token :: try consume(state, .ident)
name = try ast.add_identifier(state.builder, token.span, token.data.str_id)
}
if (declaration) {
type = try parse_type(state)
if next_is(state, .equal) {
state.next_token += 1
value = try parse_expr(state)
}
} else {
if (named) _ = try consume(state, .equal)
value = try parse_expr(state)
}
end :: if (state.next_token > start_token) state.tokens[state.next_token - 1].span.end else start.end
return try ast.add_field(state.builder, ast.Span{
start = start.start,
end = end,
source = start.source,
}, name, type, value)
}
@hide:file
recover_record_field proc(state @mut State, start usize) ast.Span {
depth usize := 0
for (start..state.next_token) |index| {
kind :: state.tokens[index].kind
if (kind == .open_curly or kind == .open_paren or kind == .open_bracket) depth += 1
if ((kind == .close_curly or kind == .close_paren or kind == .close_bracket) and depth > 0) depth -= 1
}
while (!next_is(state, .eof)) {
kind :: current(state).kind
if (depth == 0 and (kind == .comma or kind == .newline or kind == .close_curly)) break
if (kind == .open_curly or kind == .open_paren or kind == .open_bracket) depth += 1
if (kind == .close_curly or kind == .close_paren or kind == .close_bracket) {
if (depth == 0) break
depth -= 1
}
state.next_token += 1
}
first :: state.tokens[start].span
end :: if (state.next_token > start) state.tokens[state.next_token - 1].span.end else first.end
return ast.Span{
start = first.start,
end = end,
source = first.source,
}
}
@hide:file
invalid_record proc(state @mut State, start u32, recover_tail bool) ast.NodeId ! ParseError {
diagnostic :: try unexpected_diagnostic(state)
if (recover_tail) {
while (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
before :: state.next_token
_ = recover_record_field(state, before)
if (next_is(state, .comma)) state.next_token += 1
if (state.next_token == before) break
}
if (next_is(state, .close_curly)) state.next_token += 1
}
return try ast.add_invalid_expr(state.builder, ast.Span{
start = start,
end = state.tokens[state.next_token - 1].span.end,
source = state.builder.source_id,
}, diagnostic)
}
@hide:file
@@ -658,10 +829,18 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .open_curly and state.allow_record_literals) {
left = try parse_record_literal(state, left, state.tokens[expression_start].span.start)
left_grouped = false
continue
}
if (token.kind == .dot) {
state.next_token += 1
member :: try consume(state, .ident)
member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id)
member :: try consume_either(state, &[.ident, .int])
member_id :: if (member.kind == .int)
try ast.add_literal(state.builder, .int, member.span)
else
try ast.add_identifier(state.builder, member.span, member.data.str_id)
left = try ast.add_selection(
state.builder,
ast.Span{
@@ -736,11 +915,18 @@ validate_range_bound proc(
@hide:file
parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! ParseError {
_ = try consume(state, .open_paren)
previous :: state.allow_record_literals
state.allow_record_literals = true
defer state.allow_record_literals = previous
state.paren_depth += 1
defer state.paren_depth -= 1
skip_operand_newlines(state)
arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&arguments)
argument_start :: state.next_token
node_count :: state.builder.storage.nodes.items.len
extra_count :: state.builder.storage.extra.items.len
diagnostic_count :: state.diagnostics.list.items.len
while (!is_call_boundary(current(state).kind)) {
argument :: try parse_expr(state)
@@ -749,6 +935,15 @@ parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! Par
state.next_token += 1
skip_operand_newlines(state)
}
if (!previous and !next_is(state, .close_paren)) {
# a guard's body may look like a record argument until its missing ')' is known.
state.next_token = argument_start
state.builder.storage.nodes.items = state.builder.storage.nodes.items.ptr[..node_count]
state.builder.storage.spans.items = state.builder.storage.spans.items.ptr[..node_count]
state.builder.storage.extra.items = state.builder.storage.extra.items.ptr[..extra_count]
state.diagnostics.list.items = state.diagnostics.list.items.ptr[..diagnostic_count]
return .unexpected_token
}
close :: try consume(state, .close_paren)
return try ast.add_call(
state.builder,
@@ -765,6 +960,9 @@ parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! Par
@hide:file
parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! ParseError {
_ = try consume(state, .open_bracket)
previous :: state.allow_record_literals
state.allow_record_literals = true
defer state.allow_record_literals = previous
index :: try parse_expr(state)
if !next_is(state, .close_bracket) {
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
@@ -801,7 +999,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
return next == .ident or next == .mut or next == .open_bracket or next == .struct
}
}
if (kind == .open_curly or kind == .close_curly or kind == .eof) return false
@@ -812,6 +1010,9 @@ array_starts_type proc(state @State) bool {
@hide:file
parse_array_literal proc(state @mut State) ast.NodeId ! ParseError {
open :: try consume(state, .open_bracket)
previous :: state.allow_record_literals
state.allow_record_literals = true
defer state.allow_record_literals = previous
elements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&elements)
after_open :: state.next_token
@@ -871,12 +1072,21 @@ named_binding_starts proc(state @State) bool {
parse_primary proc(state @mut State) ast.NodeId ! ParseError {
token :: current(state)
match token.kind {
.struct: return try parse_record_type(state)
.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)
return try ast.add_invalid_expr(state.builder, token.span, diagnostic)
}
.open_bracket: {
if array_starts_type(state) return try parse_type(state)
return try parse_array_literal(state)
}
.open_paren: {
state.next_token += 1
previous :: state.allow_record_literals
state.allow_record_literals = true
defer state.allow_record_literals = previous
state.paren_depth += 1
defer state.paren_depth -= 1
expression :: try parse_expr(state)
@@ -943,7 +1153,7 @@ is_call_boundary proc(kind lexer.TokenKind) bool {
return (
kind == .break
or kind == .continue
or is_expression_boundary(kind)
or (kind != .open_curly and is_expression_boundary(kind))
)
}
-35
View File
@@ -745,41 +745,6 @@ rejects_top_level_assignment_loop_else_and_assignment_expressions test {
try testing.expect_equal("kept", ast.source_text(&tree, kept.name))
}
recovers_nested_braces_in_malformed_loop_body_and_assignment test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` while (true) @ {
` bad()
` }
` x = {
` bad()
` }
` kept :: 7
`}
`tail :: 8
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(2, ast.package_decl_count(&tree))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(3, ast.block_statement_count(&tree, body))
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0)))
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1)))
kept :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal("kept", ast.source_text(&tree, kept.name))
tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal("tail", ast.source_text(&tree, tail.name))
}
parses_logical_precedence_and_associativity test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
+484 -25
View File
@@ -7,25 +7,39 @@ import "@source/diagnostics"
sema :: import "@source/sema"
TypeKind :: enum {
none,
invalid,
int,
float,
bool,
string,
void,
function,
range,
array,
usize,
i8,
i16,
i32,
isize,
u8,
u16,
u32,
u64,
none
invalid
int
float
bool
string
void
function
range
array
record
type_value
u8, u16, u32, u64, usize
i8, i16, i32, isize
}
RecordId :: distinct u32
no_record :: maxval!(RecordId)
RecordField :: struct {
name ?ast.NodeId
type Type
default ?ast.NodeId
}
RecordType :: struct {
declaration ast.NodeId
fields []RecordField
}
@hide:file
RecordInitializer :: struct {
fields []mut ast.NodeId
}
ArrayId :: distinct u32
@@ -43,17 +57,21 @@ Type :: struct {
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
array_type ArrayId = no_array
range_element TypeKind = .none
record_type RecordId = no_record
}
@hide:file
Storage :: struct {
types []mut Type
arrays arraylist.ArrayList(ArrayType)
records arraylist.ArrayList(RecordType)
initializers arraylist.ArrayList(RecordInitializer)
slots []mut usize
fallthroughs hashmap.AutoHashMap(ast.NodeId, diagnostics.DiagnosticId)
allocator mem.Allocator
}
#! owns node-indexed types, interned array descriptors, and function fallthrough obligations; release with deinit.
#! owns node-indexed types, aggregate descriptors, initializer mappings, and function fallthrough obligations; release with deinit.
#! function IDs refer to the caller-owned AST; diagnostic IDs refer to the caller-owned store.
Result :: struct { storage Storage }
@@ -104,6 +122,8 @@ Cell :: struct {
literal ast.NodeId = ast.NodeId(maxval!(u32))
producer ast.NodeId = ast.NodeId(maxval!(u32))
capture ast.NodeId = ast.NodeId(maxval!(u32))
record_checking bool = false
nominal bool = false
first_edge usize = no_index
return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
}
@@ -124,12 +144,15 @@ State :: struct {
groups []mut GroupLink
edges arraylist.ArrayList(Edge)
arrays arraylist.ArrayList(ArrayType)
records arraylist.ArrayList(RecordType)
initializers arraylist.ArrayList(RecordInitializer)
slots []mut usize
collecting_assignment bool = false
has_assignments bool = false
}
#! borrows the AST, resolution, and diagnostic store only during checking.
#! temporary constraints are freed on every exit; Result owns types, array descriptors, and fallthrough obligations.
#! temporary constraints are freed on every exit; Result owns types, aggregate descriptors, initializer mappings, and fallthrough obligations.
check proc(
allocator mem.Allocator,
tree @ast.Tree,
@@ -149,6 +172,9 @@ check proc(
for (0..count) |i| groups[i].parent = i
queue :: try mem.alloc(ast.NodeId, allocator, count)
defer mem.free(allocator, queue)
slots :: try mem.alloc(usize, allocator, count)
errdefer mem.free(allocator, slots)
mem.set(slots, no_index)
state := State{
tree = tree,
resolution = resolution,
@@ -158,9 +184,18 @@ check proc(
groups = groups,
edges = arraylist.init(allocator),
arrays = arraylist.init(allocator),
records = arraylist.init(allocator),
initializers = arraylist.init(allocator),
slots = slots,
}
defer arraylist.deinit(&state.edges)
errdefer arraylist.deinit(&state.arrays)
errdefer {
for (state.records.items) |record| mem.free(allocator, record.fields)
arraylist.deinit(&state.records)
for (state.initializers.items) |initializer| mem.free(allocator, initializer.fields)
arraylist.deinit(&state.initializers)
}
for 0..ast.package_decl_count(tree) |i| {
id :: ast.package_decl(tree, i)
@@ -195,6 +230,7 @@ check proc(
for (0..count) |i| {
id :: ast.NodeId(i)
if (cells[i].role != .expression or !declaration_reference(tree, id)) continue
if (cells[i].type.kind == .invalid) continue
binding :: sema.binding(resolution, id)
if (binding.kind != .declaration) continue
target :: binding.data.declaration
@@ -207,6 +243,7 @@ check proc(
join(&state, id, ast.statement_decl(tree, id).initializer)
}
if (cells[i].role != .expression or !declaration_reference(tree, id)) continue
if (cells[i].type.kind == .invalid) continue
binding :: sema.binding(resolution, id)
if (binding.kind == .declaration and !cells[i].assignment_value and !cells[usize(binding.data.declaration)].fixed) {
join(&state, id, binding.data.declaration)
@@ -225,6 +262,7 @@ check proc(
or cells[i].role != .expression
or !declaration_reference(tree, id)
) continue
if (cells[i].type.kind == .invalid) continue
binding :: sema.binding(resolution, id)
if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)]
}
@@ -356,6 +394,11 @@ check proc(
}
}
for (0..count) |i| try validate(&state, ast.NodeId(i))
for state.records.items |record| {
for record.fields |field| {
if (field.default) |value| try compatible(&state, value, field.type)
}
}
# errors travel only from dependencies to users. A rejected use never poisons
# an explicitly typed declaration, and body errors do not erase signatures.
@@ -401,6 +444,9 @@ check proc(
return Result{ storage = Storage{
types = types,
arrays = state.arrays,
records = state.records,
initializers = state.initializers,
slots = slots,
fallthroughs = fallthroughs,
allocator = allocator,
} }
@@ -427,11 +473,36 @@ array_type proc(result @Result, id ArrayId) ArrayType {
return result.storage.arrays.items[usize(id)]
}
record_type_count proc(result @Result) usize {
return result.storage.records.items.len
}
record_type proc(result @Result, id RecordId) RecordType {
debug.assert(usize(id) < result.storage.records.items.len)
return result.storage.records.items[usize(id)]
}
field_index proc(result @Result, selection ast.NodeId) usize {
debug.assert(result.storage.slots[usize(selection)] != no_index)
return result.storage.slots[usize(selection)]
}
initializer_field proc(result @Result, literal ast.NodeId, index usize) ast.NodeId {
slot :: result.storage.slots[usize(literal)]
debug.assert(slot != no_index)
return result.storage.initializers.items[slot].fields[index]
}
#! releases type, descriptor, and fallthrough storage using the allocator retained by check.
deinit proc(result @mut Result) void {
mem.free(result.storage.allocator, result.storage.types)
arraylist.deinit(&result.storage.arrays)
hashmap.deinit(&result.storage.fallthroughs)
for (result.storage.records.items) |record| mem.free(result.storage.allocator, record.fields)
arraylist.deinit(&result.storage.records)
for (result.storage.initializers.items) |initializer| mem.free(result.storage.allocator, initializer.fields)
arraylist.deinit(&result.storage.initializers)
mem.free(result.storage.allocator, result.storage.slots)
}
@hide:file
@@ -518,6 +589,7 @@ join proc(state @mut State, left, right ast.NodeId) void {
state.cells[a].numeric = state.cells[a].numeric or state.cells[b].numeric
state.cells[a].anchored = state.cells[a].anchored or state.cells[b].anchored
if (state.cells[a].demand == .none) state.cells[a].demand = state.cells[b].demand
if (state.cells[a].expected.kind == .none) state.cells[a].expected = state.cells[b].expected
state.cells[a].conversion_unsigned = state.cells[a].conversion_unsigned or state.cells[b].conversion_unsigned
if (state.cells[a].literal == ast.NodeId(maxval!(u32))) state.cells[a].literal = state.cells[b].literal
if (state.cells[a].producer == ast.NodeId(maxval!(u32))) state.cells[a].producer = state.cells[b].producer
@@ -540,6 +612,12 @@ collect_invalid proc(state @mut State, id ast.NodeId) void {
check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (mem.AllocError | diagnostics.CapacityError) {
if (state.cells[usize(id)].role == .annotation) return state.types[usize(id)]
state.cells[usize(id)].role = .annotation
if (ast.expr_kind(state.tree, id) == .record_type) {
type :: try check_record(state, id, false)
state.types[usize(id)] = type
state.cells[usize(id)].type = type
return type
}
if (ast.expr_kind(state.tree, id) == .array_type) {
view :: ast.array_type(state.tree, id)
element := try check_annotation(state, view.element, true)
@@ -582,6 +660,20 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_void_type, ast.node_span(state.tree, id)))
}
}
.declaration: {
target :: binding.data.declaration
role :: state.cells[usize(target)].role
if (role != .parameter and role != .loop_capture and ast.decl_kind(state.tree, target) == .statement) {
initializer :: ast.statement_decl(state.tree, target).initializer
if (ast.expr_kind(state.tree, initializer) == .record_type) {
type = try check_record(state, initializer, true)
} else {
type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id)))
}
} else {
type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id)))
}
}
else: type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id)))
}
state.types[usize(id)] = type
@@ -593,6 +685,9 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
collect_declaration proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
declaration :: ast.statement_decl(state.tree, id)
state.cells[usize(id)].role = .declaration
if (ast.expr_kind(state.tree, declaration.initializer) == .record_type) {
state.cells[usize(declaration.initializer)].nominal = true
}
if (declaration.type) |type| {
state.cells[usize(id)].fixed = true
state.cells[usize(id)].type = try check_annotation(state, type, false)
@@ -604,6 +699,11 @@ collect_declaration proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
}
try collect_expression(state, declaration.initializer)
if (ast.expr_kind(state.tree, declaration.initializer) == .record_type
and state.cells[usize(id)].type.kind != .invalid) {
state.cells[usize(id)].fixed = true
state.cells[usize(id)].type = state.cells[usize(declaration.initializer)].type
}
try depend(state, declaration.initializer, id)
}
@@ -772,6 +872,19 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
.identifier, .selection: {
binding :: sema.binding(state.resolution, id)
if (binding.kind == .none and ast.expr_kind(tree, id) == .selection) {
base :: ast.selection_expr(tree, id).base
try collect_expression(state, base)
try depend(state, base, id)
}
if (binding.kind == .declaration) {
target :: binding.data.declaration
role :: state.cells[usize(target)].role
if (role != .parameter and role != .loop_capture and ast.decl_kind(tree, target) == .statement
and ast.expr_kind(tree, ast.statement_decl(tree, target).initializer) == .record_type) {
state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id)))
}
}
match binding.kind {
.invalid: state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
.builtin: state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id)))
@@ -839,6 +952,23 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
.array_type: {
state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id)))
}
.record_type: {
type := try check_record(state, id, state.cells[usize(id)].nominal)
if (type.kind == .record) type.kind = .type_value
state.cells[usize(id)].type = type
}
.record_literal: {
state.cells[usize(id)].literal = id
if (ast.record_literal(tree, id).type) |annotation| {
state.cells[usize(id)].expected = try check_annotation(state, annotation, false)
try depend(state, annotation, id)
}
for 0..ast.record_field_count(tree, id) |i| {
value :: ast.field(tree, ast.record_field(tree, id, i)).value?
try collect_expression(state, value)
try depend(state, value, id)
}
}
.array_literal: {
state.cells[usize(id)].literal = id
for 0..ast.array_element_count(tree, id) |i| {
@@ -992,9 +1122,24 @@ numeric_constraint proc(state @mut State, expression, operand, operation ast.Nod
demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code) void ! (mem.AllocError | diagnostics.CapacityError) {
if (state.types[usize(id)].kind == .invalid) return
index :: root(state, usize(id))
if (expected.kind == .record) {
literal :: state.cells[index].literal
if (literal == ast.NodeId(maxval!(u32)) or ast.expr_kind(state.tree, literal) != .record_literal) return
if (ast.record_literal(state.tree, literal).type == null) {
previous :: state.cells[index].expected
if (previous.kind == .record and !same_type(state, previous, expected)) {
try fail(state, literal, code, ast.node_span(state.tree, literal))
return
}
state.cells[index].expected = expected
}
_ = try infer_record_literal(state, literal, index, false)
return
}
if (expected.kind == .array) {
literal :: state.cells[index].literal
if (literal == ast.NodeId(maxval!(u32))) return
if (ast.expr_kind(state.tree, literal) != .array_literal) return
previous :: state.cells[index].expected
if (previous.kind == .array and !same_type(state, previous, expected)) {
if (!has_inferred_count(state, previous) and !has_inferred_count(state, expected)) {
@@ -1050,6 +1195,11 @@ mutable_target proc(state @State, id ast.NodeId) bool {
base :: state.types[usize(view.base)]
return base.kind == .array and state.arrays.items[usize(base.array_type)].mutable and mutable_target(state, view.base)
}
if (ast.expr_kind(state.tree, id) == .selection
and sema.binding(state.resolution, id).kind == .none) {
view :: ast.selection_expr(state.tree, id)
return state.types[usize(view.base)].kind == .record and mutable_target(state, view.base)
}
binding :: sema.binding(state.resolution, id)
if (binding.kind != .declaration) return false
target :: binding.data.declaration
@@ -1178,6 +1328,18 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
}
if (array_cells(state, actual) == null) try fail(state, id, .array_size_overflow, ast.node_span(tree, id))
}
.record_literal: {
actual :: state.types[usize(id)]
if (actual.kind != .record) return
fields :: state.records.items[usize(actual.record_type)].fields
slot :: state.slots[usize(id)]
if (slot == no_index) return
values :: state.initializers.items[slot].fields
for fields |field, i| {
if (values[i] != ast.NodeId(maxval!(u32))) try compatible(state, values[i], field.type)
}
if (array_cells(state, actual) == null) try fail(state, id, .array_size_overflow, ast.node_span(tree, id))
}
.index: {
view :: ast.index_expr(tree, id)
base :: state.types[usize(view.base)]
@@ -1334,6 +1496,7 @@ intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError
if (existing.len == descriptor.len and existing.mutable == descriptor.mutable
and existing.element.kind == descriptor.element.kind
and existing.element.function == descriptor.element.function
and existing.element.record_type == descriptor.element.record_type
and existing.element.array_type == descriptor.element.array_type) {
return Type{ kind = .array, array_type = ArrayId(i) }
}
@@ -1346,6 +1509,22 @@ intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError
same_type proc(state @State, left, right Type) bool {
if (left.kind != right.kind) return false
if (left.kind == .function) return same_signature(state, left.function, right.function)
if (left.kind == .record or left.kind == .type_value) {
if (left.record_type == right.record_type) return true
a :: state.records.items[usize(left.record_type)]
b :: state.records.items[usize(right.record_type)]
if (a.declaration != ast.NodeId(maxval!(u32)) or b.declaration != ast.NodeId(maxval!(u32))
or a.fields.len != b.fields.len) return false
for a.fields |field, i| {
other :: b.fields[i]
if ((field.name == null) != (other.name == null)) return false
if (field.name) |name| {
if (ast.name_id(state.tree, name) != ast.name_id(state.tree, other.name?)) return false
}
if (!same_type(state, field.type, other.type)) return false
}
return true
}
if (left.kind != .array) return true
a :: state.arrays.items[usize(left.array_type)]
b :: state.arrays.items[usize(right.array_type)]
@@ -1354,6 +1533,16 @@ same_type proc(state @State, left, right Type) bool {
@hide:file
array_cells proc(state @State, type Type) ?usize {
if (type.kind == .record) {
total := usize(0)
for state.records.items[usize(type.record_type)].fields |field| {
child := usize(0)
if (array_cells(state, field.type)) |value| child = value else return null
if (total > maxval!(usize) - child) return null
total += child
}
return total
}
if (type.kind != .array) return usize(1)
view :: state.arrays.items[usize(type.array_type)]
if (view.len == no_index) return null
@@ -1366,7 +1555,7 @@ array_cells proc(state @State, type Type) ?usize {
@hide:file
array_origin proc(state @mut State, id ast.NodeId) ast.NodeId {
origin :: state.cells[root(state, usize(id))].literal
if (origin != ast.NodeId(maxval!(u32))) return origin
if (origin != ast.NodeId(maxval!(u32)) and ast.expr_kind(state.tree, origin) == .array_literal) return origin
if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .index) {
base :: array_origin(state, ast.index_expr(state.tree, id).base)
if (base != ast.NodeId(maxval!(u32)) and ast.array_element_count(state.tree, base) != 0) {
@@ -1396,8 +1585,8 @@ unify_elements proc(state @mut State, left, right ast.NodeId) void ! (mem.AllocE
if (state.cells[left_root].numeric and state.cells[right_root].numeric) {
try numeric_constraint(state, left, right, right)
}
if (left_type.kind == .array) try demand(state, right, left_type, .type_mismatch)
if (right_type.kind == .array) try demand(state, left, right_type, .type_mismatch)
if (left_type.kind == .array or left_type.kind == .record) try demand(state, right, left_type, .type_mismatch)
if (right_type.kind == .array or right_type.kind == .record) try demand(state, left, right_type, .type_mismatch)
}
@hide:file
@@ -1414,6 +1603,48 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
return group_type(state, id)
}
}
if (state.cells[usize(id)].role == .expression
and ast.expr_kind(state.tree, id) == .selection
and sema.binding(state.resolution, id).kind == .none) {
view :: ast.selection_expr(state.tree, id)
base :: try infer_shape(state, view.base, defaults)
if (base.kind == .record) {
fields :: state.records.items[usize(base.record_type)].fields
slot :: selected_field(state, fields, view.member)
if (slot != no_index) {
state.slots[usize(id)] = slot
seed(state, id, fields[slot].type)
return fields[slot].type
}
try fail(state, id, .unknown_field, ast.node_span(state.tree, view.member))
} else if (base.kind == .invalid) {
seed(state, id, base)
} else if (base.kind != .none) {
try fail(state, id, .not_record, ast.node_span(state.tree, view.base))
}
if (base.kind == .none) {
origin :: state.cells[root(state, usize(view.base))].literal
if (origin != ast.NodeId(maxval!(u32)) and ast.expr_kind(state.tree, origin) == .record_literal) {
for 0..ast.record_field_count(state.tree, origin) |i| {
field :: ast.field(state.tree, ast.record_field(state.tree, origin, i))
matches := false
if (field.name) |name| {
matches = (
ast.expr_kind(state.tree, view.member) == .identifier
and ast.name_id(state.tree, name) == ast.name_id(state.tree, view.member)
)
} else if (constant_integer(state, view.member)) |ordinal| {
matches = !ordinal.negative and ordinal.magnitude == u64(i)
}
if (!matches) continue
child :: field.value?
if (state.cells[root(state, usize(child))].numeric) try numeric_constraint(state, id, child, id)
break
}
}
}
return group_type(state, id)
}
if (state.cells[usize(id)].role == .expression and declaration_reference(state.tree, id)) {
binding :: sema.binding(state.resolution, id)
if (binding.kind == .declaration) {
@@ -1431,6 +1662,9 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
}
}
literal :: state.cells[index].literal
if (literal != ast.NodeId(maxval!(u32)) and ast.expr_kind(state.tree, literal) == .record_literal) {
return try infer_record_literal(state, literal, index, defaults)
}
if (literal != ast.NodeId(maxval!(u32))) {
len :: ast.array_element_count(state.tree, literal)
expected :: state.cells[index].expected
@@ -1495,6 +1729,14 @@ prepare_shapes proc(state @mut State, assignment_values, defaults bool) void ! (
@hide:file
apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
if (!assignment_values) {
for 0..(state.records.items.len) |i| {
record :: state.records.items[i]
for record.fields |field| {
if (field.default) |value| try context_demand(state, value, field.type, .type_mismatch, early)
}
}
}
for 0..(state.cells.len) |i| {
id :: ast.NodeId(i)
if (!assignment_values and state.cells[i].role == .declaration) {
@@ -1535,7 +1777,7 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem
@hide:file
context_demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code, early bool) void ! (mem.AllocError | diagnostics.CapacityError) {
if (early and expected.kind != .array and !integer_kind(expected.kind)) return
if (early and expected.kind != .array and expected.kind != .record and !integer_kind(expected.kind)) return
try demand(state, id, expected, code)
}
@@ -1654,3 +1896,220 @@ specialize_annotations proc(state @mut State) void ! (mem.AllocError | diagnosti
}
}
}
@hide:file
named_field proc(tree @ast.Tree, fields []RecordField, name ast.NodeId) usize {
for fields |field, i| {
if (field.name) |existing| {
if (ast.name_id(tree, existing) == ast.name_id(tree, name)) return i
}
}
return no_index
}
@hide:file
selected_field proc(state @State, fields []RecordField, member ast.NodeId) usize {
if (ast.expr_kind(state.tree, member) == .identifier) return named_field(state.tree, fields, member)
value := usize(0)
for ast.source_text(state.tree, ast.node_span(state.tree, member)) |byte| {
if (byte < '0' or byte > '9') return no_index
digit :: usize(byte - '0')
if (value > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index
value = value * 10 + digit
}
if (value >= fields.len or fields[value].name != null) return no_index
return value
}
@hide:file
intern_record proc(state @mut State, declaration ast.NodeId, fields []mut RecordField) Type ! mem.AllocError {
for state.records.items |existing, i| {
if (existing.declaration != declaration or existing.fields.len != fields.len) continue
equal := true
same_defaults := true
for fields |field, j| {
slot := j
if (field.name) |name| slot = named_field(state.tree, existing.fields, name)
if (slot == no_index or (field.name == null and existing.fields[slot].name != null)
or !same_type(state, field.type, existing.fields[slot].type)) {
equal = false
break
}
other_default :: existing.fields[slot].default
if ((field.default == null) != (other_default == null)) {
same_defaults = false
} else if (field.default) |value| {
if (value != other_default?) same_defaults = false
}
}
if (equal and same_defaults) {
mem.free(state.records.allocator, fields)
return Type{ kind = .record, record_type = RecordId(i) }
}
if (equal) {
for existing.fields |field, j| {
if (field.name) |name| {
slot :: named_field(state.tree, fields, name)
saved :: fields[j]
fields[j] = fields[slot]
fields[slot] = saved
}
}
}
}
try arraylist.append(&state.records, RecordType{ declaration = declaration, fields = fields })
return Type{ kind = .record, record_type = RecordId(state.records.items.len - 1) }
}
@hide:file
check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) {
cached := state.cells[usize(id)].type
if (cached.kind == .record or cached.kind == .type_value) {
cached.kind = .record
return cached
}
if (cached.kind == .invalid) return cached
if (state.cells[usize(id)].record_checking) {
type :: invalid_type(try diagnostics.add(state.diagnostics, .recursive_record, ast.node_span(state.tree, id)))
state.cells[usize(id)].type = type
return type
}
state.cells[usize(id)].record_checking = true
defer state.cells[usize(id)].record_checking = false
count :: ast.record_field_count(state.tree, id)
fields :: try mem.alloc(RecordField, state.records.allocator, count)
owned := true
defer {
if (owned) mem.free(state.records.allocator, fields)
}
failure := Type{}
for 0..count |i| {
node :: ast.record_field(state.tree, id, i)
view :: ast.field(state.tree, node)
type := try check_annotation(state, view.type?, false)
if (type.kind == .void or type.kind == .range or type.kind == .type_value or type.kind == .none) {
type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_record_field_type, view.span))
}
if (view.name) |name| {
if (named_field(state.tree, fields[0..i], name) != no_index) {
type = invalid_type(try diagnostics.add(state.diagnostics, .duplicate_field, ast.node_span(state.tree, name)))
}
}
fields[i] = RecordField{ name = view.name, type = type, default = view.value }
if (type.kind == .invalid) failure = type
}
if (failure.kind == .invalid) {
state.cells[usize(id)].type = failure
return failure
}
declaration :: if (nominal) id else ast.NodeId(maxval!(u32))
type :: try intern_record(state, declaration, fields)
owned = false
state.cells[usize(id)].type = type
state.cells[usize(id)].record_checking = false
saved_assignment :: state.collecting_assignment
state.collecting_assignment = false
defer state.collecting_assignment = saved_assignment
for 0..count |i| {
if (ast.field(state.tree, ast.record_field(state.tree, id, i)).value) |value| {
try collect_expression(state, value)
}
}
return type
}
@hide:file
infer_record_literal proc(state @mut State, literal ast.NodeId, index usize, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) {
if (state.types[usize(literal)].kind == .invalid) return state.types[usize(literal)]
count :: ast.record_field_count(state.tree, literal)
expected := state.cells[index].expected
if (expected.kind == .none and state.cells[index].type.kind == .record) expected = state.cells[index].type
if (expected.kind == .invalid) {
seed(state, literal, expected)
return expected
}
if (expected.kind != .none and expected.kind != .record) {
try fail(state, literal, .expected_type, ast.node_span(state.tree, literal))
return state.types[usize(literal)]
}
if (expected.kind == .none) {
ready := true
for 0..count |i| {
value :: ast.field(state.tree, ast.record_field(state.tree, literal, i)).value?
actual :: try infer_shape(state, value, defaults)
if (actual.kind == .none) ready = false
if (actual.kind == .invalid) {
seed(state, literal, actual)
return actual
}
}
if (!ready) return Type{}
fields :: try mem.alloc(RecordField, state.records.allocator, count)
errdefer mem.free(state.records.allocator, fields)
for 0..count |i| {
view :: ast.field(state.tree, ast.record_field(state.tree, literal, i))
type := group_type(state, view.value?)
if (type.kind == .void or type.kind == .type_value or type.kind == .range) {
try fail(state, literal, .invalid_record_field_type, view.span)
mem.free(state.records.allocator, fields)
return state.types[usize(literal)]
}
if (view.name) |name| {
if (named_field(state.tree, fields[0..i], name) != no_index) {
try fail(state, literal, .duplicate_field, ast.node_span(state.tree, name))
mem.free(state.records.allocator, fields)
return state.types[usize(literal)]
}
}
fields[i] = RecordField{ name = view.name, type = type, default = null }
}
expected = try intern_record(state, ast.NodeId(maxval!(u32)), fields)
}
fields :: state.records.items[usize(expected.record_type)].fields
slot := state.slots[usize(literal)]
fresh :: slot == no_index
if (slot == no_index) {
values :: try mem.alloc(ast.NodeId, state.initializers.allocator, fields.len)
errdefer mem.free(state.initializers.allocator, values)
try arraylist.append(&state.initializers, RecordInitializer{ fields = values })
slot = state.initializers.items.len - 1
state.slots[usize(literal)] = slot
}
values :: state.initializers.items[slot].fields
if (values.len != fields.len) {
try fail(state, literal, .type_mismatch, ast.node_span(state.tree, literal))
return state.types[usize(literal)]
}
mem.set(values, ast.NodeId(maxval!(u32)))
for 0..count |i| {
node :: ast.record_field(state.tree, literal, i)
view :: ast.field(state.tree, node)
field_slot := i
if (view.name) |name| field_slot = named_field(state.tree, fields, name)
if (field_slot >= fields.len) {
code :: if (view.name != null) diagnostics.Code.unknown_field else diagnostics.Code.tuple_arity_mismatch
try fail(state, literal, code, view.span)
continue
}
if (values[field_slot] != ast.NodeId(maxval!(u32))) {
try fail(state, literal, .duplicate_field, view.span)
continue
}
values[field_slot] = view.value?
try demand(state, view.value?, fields[field_slot].type, .type_mismatch)
_ = try infer_shape(state, view.value?, defaults)
}
for fields |field, i| {
if (values[i] != ast.NodeId(maxval!(u32))) continue
if (field.default) |value| {
values[i] = value
if (fresh) try depend(state, value, literal)
} else {
code :: if (field.name == null) diagnostics.Code.tuple_arity_mismatch else diagnostics.Code.missing_field
try fail(state, literal, code, ast.node_span(state.tree, literal))
}
}
if (state.types[usize(literal)].kind == .invalid) return state.types[usize(literal)]
state.cells[index].type = expected
return expected
}
+26 -12
View File
@@ -443,20 +443,24 @@ resolve_expr proc(
expression :: ast.selection_expr(tree, id)
try resolve_expr(state, scope, expression.base)
base :: state.bindings[usize(expression.base)]
value := base
if (base.kind != .invalid) {
if (base.kind == .namespace) {
namespace :: hashmap.get(&state.namespaces, base.data.namespace)?
if hashmap.get(&namespace.names, ast.name_id(tree, expression.member)) |found| {
value = found
} else {
value = Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(state.diagnostics, .unknown_name, ast.node_span(tree, expression.member)),
} }
}
value := Binding{}
if (base.kind == .invalid) {
value = base
} else if (base.kind == .namespace) {
namespace :: hashmap.get(&state.namespaces, base.data.namespace)?
found :: if (ast.expr_kind(tree, expression.member) == .identifier)
hashmap.get(&namespace.names, ast.name_id(tree, expression.member))
else
null
if (found) |member| {
value = member
} else {
value = Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(state.diagnostics, .not_namespace, ast.node_span(tree, expression.base)),
diagnostic = try diagnostics.add(
state.diagnostics,
.unknown_name,
ast.node_span(tree, expression.member),
),
} }
}
}
@@ -490,6 +494,16 @@ resolve_expr proc(
try resolve_expr(state, scope, ast.array_element(tree, id, i))
}
}
.record_type, .record_literal: {
if (ast.expr_kind(tree, id) == .record_literal) {
if (ast.record_literal(tree, id).type) |type| try resolve_expr(state, scope, type)
}
for (0..ast.record_field_count(tree, id)) |i| {
field :: ast.field(tree, ast.record_field(tree, id, i))
if (field.type) |type| try resolve_expr(state, scope, type)
if (field.value) |value| try resolve_expr(state, scope, value)
}
}
.index: {
expression :: ast.index_expr(tree, id)
try resolve_expr(state, scope, expression.base)