feat: support enums, distinct types, unions and matching

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