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
+10 -6
View File
@@ -22,7 +22,7 @@ run a source file with the interpreter:
bare `import "../library"` and aliased `library :: import "@library"` declarations load dependencies. relative paths start at the importing package directory; `@` starts at the nearest ancestor containing `build.hon` or `build.bro`, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports.
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. imported namespaces are not re-exported; missing members and non-namespace selections are diagnosed. general record fields, imported types and visibility attributes are not implemented; unsupported attributes produce syntax diagnostics.
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. qualified record types and constructors such as `library.Point` and `library.Point { x = 1 }` use the same declaration identity. imported namespaces are not re-exported; missing members are diagnosed. visibility attributes are not implemented; unsupported attributes produce syntax diagnostics.
```hon
add proc(left, right int) int {
@@ -55,7 +55,7 @@ clang -target arm64-apple-macosx11.0.0 -O2 build/factorial.ll -o build/factorial
`emit-llvm` writes LLVM text to stdout and source diagnostics to stderr. both emission commands return `1` for recoverable source diagnostics while still producing their output when emission and linking succeed. the resulting executable preserves demand-driven traps, reports all source diagnostics, and returns `1` even if unaffected code completes. runtime-generated diagnostics retain source locations; the original source file and compiler are not needed to run the executable. invocation or Clang failures return `2`.
the backend (`@source/llvm`) consumes the existing typed IR, emits native functions and control flow, and uses entry-block stack slots for locals and instruction results, including non-SSA result IDs. LLVM can promote eligible slots during optimization. arrays use typed native aggregates with by-value copies; loop storage is allocated once per invocation. signed and unsigned arithmetic use checked overflow intrinsics, array accesses retain bounds checks, local initialization flags reset on scope re-entry, globals initialize lazily with cycle checks, and floating operations preserve IEEE behavior without fast-math flags. native calls use the native stack rather than the interpreter's heap-backed frames.
the backend (`@source/llvm`) consumes the existing typed IR, emits native functions and control flow, and uses entry-block stack slots for locals and instruction results, including non-SSA result IDs. LLVM can promote eligible slots during optimization. arrays and records use typed native aggregates with by-value copies; loop storage is allocated once per invocation. signed and unsigned arithmetic use checked overflow intrinsics, array accesses retain bounds checks, local initialization flags reset on scope re-entry, globals initialize lazily with cycle checks, and floating operations preserve IEEE behavior without fast-math flags. native calls use the native stack rather than the interpreter's heap-backed frames.
run the bootstrap suite with `bro test`. after `bro build`, run `python3 tests/native.py` for executable-level interpreter comparisons at both `-O0` and `-O2`, including traps, source diagnostics, array ownership, calling conventions, and CLI error handling.
@@ -93,6 +93,10 @@ array lengths currently support nonnegative integer literals, unary signs, `+`,
indices require `usize`; integer literals and flexible inferred values receive this context, while fixed signed integers do not convert implicitly. out-of-bounds access traps before loading or storing, including for empty arrays. reads evaluate the base before the index; indexed assignment evaluates its right-hand side before the destination, each once. leading nested assignment targets follow Bro's declaration ambiguity: write `(matrix[1])[0] = 9`, not `matrix[1][0] = 9`. [programs/array_sum.hon](programs/array_sum.hon) uses a `usize` range bound to index, mutate and sum an array; `./build/honey run programs/array_sum.hon` prints `17`.
records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default.
tuple types use `struct { i64, bool }`; tuple values use `{42, true}`, `{42,}` and `{}`. anonymous keyed records, such as `{ x = 40, ready = true }`, infer structural types. fields use `.x` or canonical tuple ordinals such as `.0`, including chained selections. mutable local roots permit field updates; constants, parameters and globals remain readonly, and traversed arrays still require mutable elements. construction, binding, assignment, arguments and returns copy nested records/tuples and arrays by value. fields can be read from returned temporaries; records containing no scalar leaves retain their logical shapes. CLI results print declaration-order values as `{value, value}`.
`while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry.
`for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' exact integer type in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum of each integer type.
@@ -113,7 +117,7 @@ resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put`
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. `sema.resolve_packages` additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types and interned array descriptors; query with `check.type_of` and `check.array_type`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types and fixed arrays; type names are not ordinary values. `void` is valid as a function result, not as a parameter or stored value.
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, array/record descriptors and construction mappings; query with `check.type_of`, `check.array_type`, `check.record_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types, fixed arrays and local/imported record types; type names are not ordinary runtime values. `void` is valid as a function result, not as a parameter or stored value.
function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage.
@@ -131,10 +135,10 @@ type errors carry source spans and diagnostic identities; invalid operands propa
`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. short-circuit expressions use branches and synthetic boolean local slots to merge results; call arguments are packed in the block reached after evaluating them. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter.
array IR retains recursive element types, counts and mutability. typed aggregate construction, element addresses and load/store instructions use flattened storage reserved per invocation; nested copies and indexed accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers.
aggregate IR retains recursive array element types/counts/mutability and declaration-order record fields with flattened offsets. typed construction, field/element addresses and load/store instructions use storage reserved per invocation; nested copies and accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers.
IR construction owns derived layout metadata. `ir.add_array` checks shape capacity and computes flattened leaf counts; `ir.add_parameter` and `ir.add_local` create slots with their disjoint array backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, array-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated array results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required.
IR construction owns derived layout metadata. `ir.add_array` and `ir.add_record` check shape capacity and compute flattened leaf counts; record shapes own copied field metadata. both aggregate kinds use `ArrayId` shapes and the existing backing segments. `ir.add_parameter` and `ir.add_local` create slots with disjoint aggregate backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, aggregate-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated aggregate results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required.
`interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned arrays own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. array descriptors also borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
`interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned aggregates own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. aggregate descriptors also borrow the module; `ir.aggregate_item` projects a borrowed field/element while preserving zero-cell child shapes. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. compound assignment remains unimplemented.
+8 -5
View File
@@ -6,8 +6,9 @@
directory packages support file-local import namespaces and postfix declaration
selection through the interpreter and native backend. concrete 8/16/32/64-bit
integers and checked numeric conversions work through both execution paths;
records and the richer types/control flow used by the compiler remain unsupported.
integers, checked numeric conversions, records/tuples, construction/defaults and
field reads/writes work through both execution paths. enums, distinct/alias types,
unions and matching remain unsupported.
the driver retains frontend storage
and source provenance through `@source/compilation`, with compilation-wide AST node identities.
file commands retain single-file behavior; directory commands compile the
@@ -28,14 +29,16 @@ needs records, pointers, generics and C calls.
- [x] verify sibling source files, transitive/shared imports, same-spelled declarations in different packages, alias conflicts, missing packages/members, argument errors and dependency-file diagnostics. preserve existing single-file commands.
visibility follows Bro's public-by-default contract; unsupported visibility
attributes must be diagnosed, never ignored. this milestone does not include
general record fields, imported types, test discovery or FFI.
attributes must be diagnosed, never ignored. the scalar-function milestone did not
include record fields or imported types; those now work for the record/tuple slice.
test discovery and FFI remain outside these completed slices.
## subsequent dependency gates
- [ ] concrete data models: required integer widths/conversions, records/tuples and construction, fields, enums, distinct/alias types, unions and matching. examples: `source/lexer/token.hon`, `source/sema/resolve.hon`, `std/mem/mem.hon`.
- [x] native integer widths (`i8`–`i64`, `u8`–`u64`, `isize`/`usize`) and checked explicit numeric conversions, including float truncation/range checks and integer-to-float rounding. preserve exact types through arrays, calls and range captures; retain strict implicit conversion rules.
- [ ] records/tuples and construction, fields, enums, distinct/alias types, unions and matching.
- [x] records/tuples, typed/contextual/anonymous construction, field defaults and reads/writes, nested aggregate value copies, imported record types and constructors through interpreter/native execution.
- [ ] enums, distinct/alias types, unions and matching.
- [ ] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`.
- [ ] fallible results and cleanup: error-set composition, `try`/`catch`, `defer`/`errdefer`; infrastructure errors remain distinct from diagnostic traps. examples: `source/main.hon:41`, `source/sema/check/check.hon:117`.
- [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage.
+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)] },
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))
}
} else {
element = value.data.elements[index]
}
try print_value(output, module, element)
}
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
}
+25 -11
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) {
value := Binding{}
if (base.kind == .invalid) {
value = base
} else 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
found :: if (ast.expr_kind(tree, expression.member) == .identifier)
hashmap.get(&namespace.names, ast.name_id(tree, expression.member))
else
null
if (found) |member| {
value = member
} else {
value = Binding{ kind = .invalid, data = BindingData{
diagnostic = try diagnostics.add(state.diagnostics, .unknown_name, ast.node_span(tree, expression.member)),
} }
}
} 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)
+204 -1
View File
@@ -11,6 +11,183 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = {
"record_defaults_and_copies": (
"""
Point :: struct { x i16, y i16 = 2 }
seed Point :: { x = 40 }
identity proc(p Point) Point { return p }
main proc() [3]i16 {
a := identity(seed)
b := a
a.x = 9
return [b.x + b.y, a.x, seed.x]
}
""", b"[42, 9, 40]\n", None,
),
"contextual_record_arguments": (
"""
Point :: struct { x u8, y u8 = 2 }
add proc(p Point) u8 { return p.x + p.y }
main proc() u8 { return add({ x = 40 }) }
""", b"42\n", None,
),
"nested_records_arrays_tuples": (
"""
Point :: struct { x i16, y i16 = 2 }
Box :: struct { points [2]mut Point, pair struct { u8, bool }, empty struct {} }
identity proc(b Box) Box { return b }
main proc() Box {
a Box := { points = [{ x = 1 }, { x = 40 }], pair = {255, true}, empty = {} }
b :: identity(a)
a.points[0].x = 9
a.pair.0 = 8
return b
}
""", b"{[{1, 2}, {40, 2}], {255, true}, {}}\n", None,
),
"anonymous_records_and_numeric_fields": (
"""
main proc() i64 {
a := { x = 40, pair = {{2, true}, false} }
b :: a
a.x = 1
return b.x + b.pair.0.0
}
""", b"42\n", None,
),
"record_temporary_fields_and_guards": (
"""
Point :: struct { x u8, y u8 = 2 }
make proc() Point { return { x = 40 } }
main proc() u8 {
flag :: { ready = true }
if flag.ready { return make().x + make().y }
return 0
}
""", b"42\n", None,
),
"zero_cell_records": (
"""
Empty :: struct {}
Box :: struct { values [3]Empty, empty Empty }
main proc() Box { return { values = [{}, {}, {}], empty = {} } }
""", b"{[{}, {}, {}], {}}\n", None,
),
"record_default_override": (
"""
Point :: struct { x i64 = 9223372036854775807 + 1 }
main proc() i64 { p :: Point { x = 42 }
return p.x }
""", b"42\n", None,
),
"record_default_trap": (
"""
Point :: struct { x i64 = 9223372036854775807 + 1 }
main proc() i64 { p :: Point {}
return p.x }
""", b"", b"integer arithmetic overflow",
),
"record_branchful_default": (
"""
Point :: struct { x bool = false and (9223372036854775807 + 1 == 0) }
main proc() bool { p :: Point {}
return !p.x }
""", b"true\n", None,
),
"record_explicit_evaluation_order": (
"""
cycle i64 :: cycle
Point :: struct { x i64, y i64 }
main proc() Point { return Point { y = 9223372036854775807 + 1, x = cycle } }
""", b"", b"integer arithmetic overflow",
),
"record_explicit_before_default": (
"""
cycle i64 :: cycle
Point :: struct { x i64 = cycle, y i64 }
main proc() Point { return Point { y = 9223372036854775807 + 1 } }
""", b"", b"integer arithmetic overflow",
),
"record_assignment_rhs_before_place": (
"""
Point :: struct { xs [1]mut i64 }
main proc() Point { p Point := { xs = [0] }
p.xs[1] = 9223372036854775807 + 1
return p }
""", b"", b"integer arithmetic overflow",
),
"record_reordered_structural_assignment": (
"main proc() int { a := { x = 1, y = 2 }\na = { y = 40, x = 2 }\nreturn a.x + a.y }",
b"42\n", None,
),
"record_local_type_and_loop": (
"""
main proc() i16 {
Point :: struct { x i16 }
p Point := { x = 0 }
for 0..100 |i| {
copy := p
copy.x = p.x + 1
p = copy
}
return p.x
}
""", b"100\n", None,
),
"record_function_field": (
"""
plus proc(n i64) i64 { return n + 1 }
minus proc(n i64) i64 { return n - 1 }
main proc() i64 { p := { f = plus }
p.f = minus
return p.f(43) }
""", b"42\n", None,
),
"record_nominal_mismatch": (
"A :: struct { x i64 }\nB :: struct { x i64 }\nmain proc() A { b :: B { x = 42 }\nreturn b }",
b"", b"",
),
"record_missing_field": (
"Point :: struct { x i64, y i64 }\nmain proc() Point { return { x = 42 } }", b"", b"",
),
"record_duplicate_field": (
"Point :: struct { x i64 }\nmain proc() Point { return { x = 1, x = 2 } }", b"", b"",
),
"record_unknown_field": (
"Point :: struct { x i64 }\nmain proc() Point { return { x = 1, z = 2 } }", b"", b"",
),
"record_wrong_field_type": (
"Point :: struct { x u8 }\nmain proc() Point { n u16 :: 42\nreturn { x = n } }", b"", b"",
),
"record_readonly_field": (
"Point :: struct { x i64 }\nmain proc() i64 { p :: Point { x = 1 }\np.x = 2\nreturn p.x }",
b"", b"",
),
"record_readonly_array_field": (
"Point :: struct { xs [1]i64 }\nmain proc() i64 { p Point := { xs = [1] }\np.xs[0] = 2\nreturn p.xs[0] }",
b"", b"",
),
"record_parameter_readonly": (
"Point :: struct { x i64 }\nmutate proc(p Point) i64 { p.x = 2\nreturn p.x }\nmain proc() i64 { return mutate({ x = 1 }) }",
b"", b"",
),
"record_recursive_layout": (
"Point :: struct { next Point }\nmain proc() int { return 42 }", b"42\n", b"",
),
"record_tuple_arity": (
"main proc() struct { i64, bool } { return {42,} }", b"", b"",
),
"record_unused_bad_body": (
"Point :: struct { x i64 }\nbad proc() Point { return { missing = 1 } }\nmain proc() i64 { return 42 }",
b"42\n", b"",
),
"record_field_bounds": (
"Point :: struct { xs [1]i64 }\nmain proc() i64 { p :: Point { xs = [42] }\nreturn p.xs[1] }",
b"", b"index out of bounds",
),
"record_type_not_value": (
"Point :: struct { x i64 }\nmain proc() i64 { Point\nreturn 42 }", b"", b"type name cannot",
),
"void": ("main proc() void {}", b"", None),
"signed_limits": (
"main proc() [2]i64 { return [-9223372036854775808, 9223372036854775807] }",
@@ -517,7 +694,7 @@ main proc() bool {
('import "../shared"\nmain proc() int { return shared.helper(1) }', b"", b"argument count"),
('import "../shared"\nmain proc() f64 { return shared.identity(true) }', b"", b"incompatible value type"),
('import "../shared"\nmain proc() int { shared\nreturn 42 }', b"", b"namespace cannot be used as a value"),
('import "../shared"\nmain proc() int { return shared.value.member }', b"", b"requires a package namespace"),
('import "../shared"\nmain proc() int { return shared.value.member }', b"", b""),
('lib :: import "../absent"\nmain proc() int { return lib.helper() }', b"", b"cannot load package directory"),
('lib :: import "../left"\nlib :: import "../right"\nmain proc() int { return lib.helper() }', b"1\n", b"duplicate name"),
('lib :: import "../shared"\nlib :: 42\nmain proc() int { return lib.helper() }', b"", b"duplicate name"),
@@ -566,6 +743,31 @@ main proc() int { return missing }
return len(cases) + 6
def check_record_packages(temporary, executable):
project = temporary / "records"
app, library = project / "app", project / "library"
app.mkdir(parents=True)
library.mkdir()
types = library / "types.hon"
types.write_text("Point :: struct { x i16, y i16 = 2 }\n")
(library / "functions.hon").write_text("identity proc(p Point) Point { return p }\n")
(app / "main.hon").write_text(textwrap.dedent("""
lib :: import "../library"
main proc() i16 {
p lib.Point := lib.Point { x = 40 }
q :: lib.identity(p)
p.x = 1
return q.x + q.y
}
""").strip() + "\n")
check(app, executable, (b"42\n", None))
types.write_text("Point :: struct { x i16, y i16 = 32767 + 1 }\n")
check(app, executable, (b"", b"integer arithmetic overflow"))
trapped = run(str(HONEY), "run", str(app))
assert trapped.stderr.startswith(f"{types.resolve()}:1:".encode()), outcome(trapped)
return 2
def check_dependency_provenance(temporary, executable):
project = temporary / "provenance"
app = project / "app"
@@ -688,6 +890,7 @@ def main():
count += 1
count += check_packages(temporary, executable)
count += check_namespace_packages(temporary, executable)
count += check_record_packages(temporary, executable)
count += check_dependency_provenance(temporary, executable)
source = ROOT / "programs/factorial.hon"
emitted = run(str(HONEY), "emit-llvm", str(source))