feat: materialize tagged-union and fallible constants
This commit is contained in:
@@ -104,9 +104,9 @@ type-producing bodies also execute scalar/enum `match` statements and type-value
|
||||
|
||||
matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, `return struct { values [2](match wide { true: i16, false: i8 }) }` generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. tagged-union payload captures, general type-valued storage and broader generic-body match specialization remain open.
|
||||
|
||||
ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples and fixed arrays from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned aggregates become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum fields, optional fields and IEEE float bits.
|
||||
ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples, fixed arrays, tagged unions and explicit fallible results from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate and fallible arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned constants become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum/optional fields and IEEE float bits. tagged unions retain their actual discriminant and only the active payload, including nested aggregates and void/zero-cell variants. fallible constants retain success/error identity, composed or payload-bearing errors and void success; `catch` can consume them in generated lengths, defaults and runtime expressions.
|
||||
|
||||
`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks, unions and fallible results cannot escape temporary evaluation as generated constants; records/arrays containing these unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, broader compile-time match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open.
|
||||
`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings, expression statements and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks and untagged unions cannot escape temporary evaluation as generated constants; active payloads or aggregate fields containing unsupported values are diagnosed rather than retaining interpreter handles. untagged unions lack an active-member identity, so selecting a source variant could discard observable bytes. executed foreign calls remain interpreter traps. general type-valued execution, broader compile-time match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open.
|
||||
|
||||
`if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use `$()` to request their evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies.
|
||||
|
||||
|
||||
@@ -19,10 +19,11 @@ explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO
|
||||
traps or compiler infrastructure failures.
|
||||
cached generic specialization and type-producing procedures support explicit/inferred
|
||||
compile-time arguments and generated record/array storage through both paths.
|
||||
ordinary procedures can compute scalar and nested record/array compile-time values
|
||||
using mutable locals, loops and recursive calls; explicit `$()` expressions use
|
||||
the same constant evaluation path. compile-time `if` selection and type comparisons
|
||||
specialize only the selected body. type-producing procedures also accept ordinary
|
||||
ordinary procedures can compute scalar, nested record/array, tagged-union and
|
||||
fallible compile-time values using mutable locals, loops and recursive calls;
|
||||
explicit `$()` expressions use the same constant evaluation path. compile-time
|
||||
`if` selection and type comparisons specialize only the selected body.
|
||||
type-producing procedures also accept ordinary
|
||||
constant inputs, cached and erased like explicit compile-time arguments. mutable
|
||||
type/value locals, direct assignments, branches, scalar/enum matching and bounded
|
||||
integer-range/while loops now generate types and storage. matches select nested
|
||||
@@ -72,7 +73,8 @@ test discovery and FFI remain outside these completed slices.
|
||||
- [x] mutable type-execution checkpoint: type-producing bodies execute mutable type/value locals, direct binding assignments, scoped blocks, if/else and early returns, integer-range/while loops, break/continue and nested/recursive producers. generated nested types, scalar-computed lengths/defaults, nominal cache identity, exact value types, overflow/guard/assignment diagnostics, imported producers and repeatable compilation work through interpreter/native execution. type-control execution shares a 4096-step bound across nested producers; scalar operations reuse checked IR execution. field/index updates, optional captures, cleanup and type-valued aggregate storage in these bodies remain open.
|
||||
- [x] type-match checkpoint: type-producing bodies execute scalar/enum match statements and direct type-valued match expressions in initializers, assignments and returns, including nested selected arms. ordinary checking/IR execution select the arm and validate subject/pattern types, ranges, overlap, unreachable arms and exhaustiveness; only the selected body is elaborated. generated storage, scoped locals, loop transfers, early returns, nominal cache identity, selected-body diagnostics, dependency provenance and repeatable compilation work through interpreter/native execution. all bodies remain parsed/name-resolved and execution shares the type-control bound. tagged-union payload captures remain open.
|
||||
- [x] nested type-match checkpoint: matches select types inside constructors, array lengths, generic arguments, local annotations, callback signatures and field defaults in type-producing bodies. selection precedes ordinary expansion, preserves literal spelling/source provenance and loop snapshots, and caches nominal results before expanding recursive fields. selected-body and pattern diagnostics, failed deferred elaboration, imported producers and repeatable compilation work through interpreter/native execution; deferred selection shares the producer's type-control bound.
|
||||
- [ ] remaining gate: general type-valued compile-time execution, broader compile-time match/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including unions, fallible results and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates.
|
||||
- [x] tagged/fallible constant checkpoint: explicit `$()` expressions, generic value parameters and type-procedure inputs materialize tagged unions and both fallible channels through interpreter/native execution. preserve active tags/payloads, nested raw aggregate projections, string ownership, void/zero-cell variants, composed/payload errors, declared channel types, normalized cache identity and repeatable compilation. `catch` computes generated lengths and runtime values. untagged unions and storage-bearing active payloads remain escape diagnostics rather than retaining temporary interpreter handles.
|
||||
- [ ] remaining gate: general type-valued compile-time execution, broader compile-time match/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates.
|
||||
- [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
|
||||
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
|
||||
- [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support.
|
||||
|
||||
+12
-2
@@ -31,8 +31,14 @@ NodeId :: distinct u32
|
||||
@hide:file
|
||||
no_node :: maxval!(NodeId)
|
||||
|
||||
#! void literals are generated constants only; there is no source void literal.
|
||||
LiteralKind :: enum {
|
||||
int, float, string, bool, null
|
||||
int
|
||||
float
|
||||
string
|
||||
bool
|
||||
null
|
||||
void
|
||||
}
|
||||
|
||||
ExprKind :: enum {
|
||||
@@ -71,6 +77,7 @@ ExprKind :: enum {
|
||||
catch_expr
|
||||
typed_constant
|
||||
comptime_expr
|
||||
literal_void
|
||||
}
|
||||
|
||||
DeclKind :: enum {
|
||||
@@ -176,6 +183,7 @@ NodeKind :: enum {
|
||||
expr_typed_constant
|
||||
expr_comptime
|
||||
literal_float_value
|
||||
literal_void
|
||||
}
|
||||
|
||||
@hide:file
|
||||
@@ -490,6 +498,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem
|
||||
.string: .literal_string
|
||||
.bool: .literal_bool
|
||||
.null: .literal_null
|
||||
.void: .literal_void
|
||||
}
|
||||
return try add_node(builder, Node{ kind = node_kind }, span)
|
||||
}
|
||||
@@ -1408,6 +1417,7 @@ expression_kind proc(kind NodeKind) ExprKind {
|
||||
.expr_typed_constant: .typed_constant
|
||||
.expr_comptime: .comptime_expr
|
||||
.literal_float_value: .literal_float
|
||||
.literal_void: .literal_void
|
||||
else: unreachable
|
||||
}
|
||||
}
|
||||
@@ -1803,7 +1813,7 @@ node_layout proc(node Node) NodeLayout {
|
||||
layout := NodeLayout{}
|
||||
match node.kind {
|
||||
.literal_int, .literal_float, .literal_float_value, .literal_string, .literal_bool, .literal_null,
|
||||
.expr_identifier, .invalid_expr: {}
|
||||
.literal_void, .expr_identifier, .invalid_expr: {}
|
||||
.expr_unary: layout.direct_children = 2
|
||||
.expr_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant,
|
||||
.expr_fallible_type, .expr_sum_type, .expr_array_type, .expr_slice_type:
|
||||
|
||||
@@ -105,12 +105,33 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
|
||||
` }
|
||||
`}
|
||||
`data proc() [2]i16 { return [40, 2] }
|
||||
`Tag :: enum(i16) { empty = -7, payload = 90 }
|
||||
`Choice :: union(Tag) { payload struct { values [2]i16, text string }, empty void }
|
||||
`Error :: union(enum) { failed struct { code i16, text string } }
|
||||
`Packet :: struct { choices [2]Choice, results [2](i16 ! Error) }
|
||||
`packet_data proc() Packet {
|
||||
` return {
|
||||
` choices = [.payload { values = [40, 2], text = "ok" }, .empty],
|
||||
` results = [40, .failed { code = 2, text = "owned" }],
|
||||
` }
|
||||
`}
|
||||
`Token proc($value Packet) type { return struct {} }
|
||||
`main proc() i16 {
|
||||
` if $(i16 != i16) { return $(i8(128)) }
|
||||
` values :: $(data())
|
||||
` box Box(i16) :: { value = identity(i16, values[0]) }
|
||||
` fixed Fixed(count()) :: { values = [box.value, 2] }
|
||||
` same Fixed(2) :: fixed
|
||||
` packet :: $(packet_data())
|
||||
` if (Token(packet_data()) != Token(packet)) return 0
|
||||
` payload :: packet.choices[0].payload
|
||||
` if (payload.values[0] != 40 or payload.values[1] != 2 or payload.text.len != 2) return 0
|
||||
` match packet.choices[1] { .empty: {}, .payload: return 0 }
|
||||
` if ((packet.results[0] catch 0) != 40) return 0
|
||||
` error_code :: packet.results[1] catch |err| match err {
|
||||
` .failed |detail|: detail.code + i16(detail.text.len),
|
||||
` }
|
||||
` if (error_code != 7) return 0
|
||||
` return identity(same.values[0]) + same.values[$(usize(1))] + same.bias
|
||||
`}
|
||||
,
|
||||
|
||||
@@ -1823,6 +1823,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
||||
.try_expr: return try lower_try(state, id)
|
||||
.catch_expr: return try lower_catch(state, id)
|
||||
.literal_null: return try null_constant(state, .pointer, span)
|
||||
.literal_void: return ir.no_value
|
||||
.slice: return try lower_slice(state, id)
|
||||
.builtin_call: return try lower_builtin(state, id)
|
||||
.record_literal: {
|
||||
|
||||
@@ -303,7 +303,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
|
||||
.defer, .errdefer: return try parse_cleanup(state, allow_else)
|
||||
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
|
||||
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct,
|
||||
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .try: {
|
||||
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .try, .dollar: {
|
||||
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)
|
||||
|
||||
@@ -1183,6 +1183,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
||||
.literal_bool: state.cells[usize(id)].type = Type{ kind = .bool }
|
||||
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
|
||||
.literal_null: state.cells[usize(id)].type = Type{ kind = .null }
|
||||
.literal_void: state.cells[usize(id)].type = Type{ kind = .void }
|
||||
.builtin_call: try collect_builtin(state, id)
|
||||
.slice: {
|
||||
view :: ast.slice_expr(tree, id)
|
||||
|
||||
@@ -532,7 +532,7 @@ resolve_expr proc(
|
||||
try resolve_expr(state, scope, value.type)
|
||||
try resolve_expr(state, scope, value.value)
|
||||
}
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null: {}
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void: {}
|
||||
.enum_literal: {}
|
||||
.distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
|
||||
.enum_type: {
|
||||
|
||||
@@ -390,8 +390,12 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
|
||||
if (type_result or view.comptime) {
|
||||
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
||||
annotation := view.type
|
||||
if (contextual_literal(state, argument))
|
||||
annotation = try argument_annotation(state, function, view.type, arguments)
|
||||
contextual := contextual_literal(state, argument)
|
||||
if (!contextual and !is_type(state, annotation)) {
|
||||
type :: try canonical_type(state, annotation)
|
||||
contextual = type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type
|
||||
}
|
||||
if (contextual) annotation = try argument_annotation(state, function, view.type, arguments)
|
||||
arguments[i] = try evaluate_expected(state, argument, annotation)
|
||||
if (arguments[i].kind == .invalid) {
|
||||
try reject_evaluation(state, call)
|
||||
@@ -726,21 +730,14 @@ argument_annotation proc(
|
||||
|
||||
@hide:file
|
||||
evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error {
|
||||
if (contextual_literal(state, id)) {
|
||||
if (!is_type(state, annotation)) {
|
||||
type :: try canonical_type(state, annotation)
|
||||
if (type != no_node) return try evaluate_ir(state, id, type)
|
||||
if (type != no_node and (contextual_literal(state, id) or ast.expr_kind(&state.tree, type) == .fallible_type))
|
||||
return try evaluate_ir(state, id, type)
|
||||
}
|
||||
return try evaluate(state, id)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
aggregate_child proc(module @ir.Module, parent ir.Value, type ir.Type, shape ir.ArrayId, offset usize) ir.Value {
|
||||
if (!ir.aggregate(type)) return parent.data.elements[offset]
|
||||
cells :: module.arrays.items[usize(shape)].cells
|
||||
return ir.Value{ type = type, array_type = shape,
|
||||
data = ir.ValueData{ elements = parent.data.elements[offset..offset + cells] } }
|
||||
}
|
||||
|
||||
# Values leaving the temporary interpreter must own no interpreter addresses or module IDs.
|
||||
# Rebuild contextual source literals while the module and its escaped flat cells are alive.
|
||||
@hide:file
|
||||
@@ -751,12 +748,10 @@ serialize_value proc(
|
||||
result := no_node
|
||||
if (type.kind == .array) {
|
||||
array :: typecheck.array_type(checked, type.array_type)
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
stride :: if (ir.aggregate(shape.element)) module.arrays.items[usize(shape.element_array)].cells else usize(1)
|
||||
elements := arraylist.init(ast.NodeId, state.allocator)
|
||||
defer arraylist.deinit(&elements)
|
||||
for (0..array.len) |i| {
|
||||
child :: aggregate_child(module, value, shape.element, shape.element_array, i * stride)
|
||||
child :: ir.aggregate_item(module, value, i)
|
||||
node :: try serialize_value(state, module, checked, array.element, child, span)
|
||||
if (node == no_node) return no_node
|
||||
try arraylist.append(&elements, node)
|
||||
@@ -764,13 +759,18 @@ serialize_value proc(
|
||||
result = try ast.add_array_literal(state.builder, span, elements.items)
|
||||
} else if (type.kind == .record) {
|
||||
record :: typecheck.record_type(checked, type.record_type)
|
||||
if (record.is_union) return no_node
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
# Untagged storage has no active member; choosing one could discard bytes.
|
||||
if (record.is_union and record.tag_type.kind != .enumeration) return no_node
|
||||
fields := arraylist.init(ast.NodeId, state.allocator)
|
||||
defer arraylist.deinit(&fields)
|
||||
for (record.fields) |field, i| {
|
||||
layout :: shape.fields[i]
|
||||
child :: aggregate_child(module, value, layout.type, layout.array_type, layout.offset)
|
||||
if (record.is_union and !ir.union_active(module, value, i)) continue
|
||||
if (record.is_union and field.type.kind == .void) {
|
||||
result = try ast.add_enum_literal(state.builder, span, field.name?)
|
||||
try refresh(state)
|
||||
return result
|
||||
}
|
||||
child :: ir.aggregate_item(module, value, i)
|
||||
node :: try serialize_value(state, module, checked, field.type, child, span)
|
||||
if (node == no_node) return no_node
|
||||
generated :: try ast.add_field(state.builder, span, field.name, null, node)
|
||||
@@ -778,6 +778,23 @@ serialize_value proc(
|
||||
try arraylist.append(&fields, generated)
|
||||
}
|
||||
result = try ast.add_record_literal(state.builder, span, null, fields.items)
|
||||
} else if (type.kind == .fallible) {
|
||||
container :: typecheck.array_type(checked, type.array_type)
|
||||
failed :: ir.union_tag(module, value).data.integer == 1
|
||||
index :: if (failed) usize(1) else usize(0)
|
||||
channel :: if (failed) container.error_type else container.element
|
||||
child :: ir.aggregate_item(module, value, index)
|
||||
result = try serialize_value(state, module, checked, channel, child, span)
|
||||
if (result == no_node) return no_node
|
||||
# Keep named success/error types distinct even when their members overlap.
|
||||
annotation := no_node
|
||||
if (channel.nominal != no_node) annotation = channel.nominal
|
||||
else if (channel.kind == .enumeration) annotation = typecheck.enum_type(checked, channel.enum_type).declaration
|
||||
else if (channel.kind == .record) annotation = typecheck.record_type(checked, channel.record_type).declaration
|
||||
if (annotation != no_node) {
|
||||
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
|
||||
result = try ast.add_typed_constant(state.builder, span, concrete.node, result)
|
||||
}
|
||||
} else if (type.kind == .enumeration) {
|
||||
enumeration :: typecheck.enum_type(checked, type.enum_type)
|
||||
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
|
||||
@@ -793,12 +810,10 @@ serialize_value proc(
|
||||
if (container.element.kind == .pointer or container.element.kind == .function) {
|
||||
if (value.data.unsigned != 0) return no_node
|
||||
result = try ast.add_literal(state.builder, .null, span)
|
||||
} else if (!value.data.elements[0].data.boolean) {
|
||||
} else if (!ir.aggregate_item(module, value, 0).data.boolean) {
|
||||
result = try ast.add_literal(state.builder, .null, span)
|
||||
} else {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
payload :: shape.fields[1]
|
||||
child :: aggregate_child(module, value, payload.type, payload.array_type, payload.offset)
|
||||
child :: ir.aggregate_item(module, value, 1)
|
||||
return try serialize_value(state, module, checked, container.element, child, span)
|
||||
}
|
||||
} else if (ir.integer_bits(value.type) != 0 and type.kind != .pointer) {
|
||||
@@ -812,6 +827,8 @@ serialize_value proc(
|
||||
result = try ast.add_boolean(state.builder, span, value.data.boolean)
|
||||
} else if (type.kind == .string) {
|
||||
result = try ast.add_string(state.builder, span, ir.string_bytes(module, value.data.string))
|
||||
} else if (type.kind == .void) {
|
||||
result = try ast.add_literal(state.builder, .void, span)
|
||||
}
|
||||
try refresh(state)
|
||||
return result
|
||||
@@ -821,6 +838,12 @@ serialize_value proc(
|
||||
constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
|
||||
kind :: ast.expr_kind(&state.tree, left)
|
||||
if (kind != ast.expr_kind(&state.tree, right)) return false
|
||||
if (kind == .literal_void) return true
|
||||
if (kind == .typed_constant) {
|
||||
a :: ast.typed_constant(&state.tree, left)
|
||||
b :: ast.typed_constant(&state.tree, right)
|
||||
return try same_type(state, a.type, b.type) and try constant_equal(state, a.value, b.value)
|
||||
}
|
||||
if (kind == .array_literal) {
|
||||
count :: ast.array_element_count(&state.tree, left)
|
||||
if (count != ast.array_element_count(&state.tree, right)) return false
|
||||
@@ -850,6 +873,11 @@ constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
|
||||
constant_hash proc(state @mut State, id ast.NodeId) usize ! Error {
|
||||
kind :: ast.expr_kind(&state.tree, id)
|
||||
hash :: enum_index(kind)
|
||||
if (kind == .literal_void) return hash
|
||||
if (kind == .typed_constant) {
|
||||
value :: ast.typed_constant(&state.tree, id)
|
||||
return mix(mix(hash, try type_hash(state, value.type)), try constant_hash(state, value.value))
|
||||
}
|
||||
if (kind == .array_literal) {
|
||||
result := hash
|
||||
for (0..ast.array_element_count(&state.tree, id)) |i|
|
||||
@@ -962,13 +990,13 @@ evaluate_ir proc(state @mut State, expression, result_type ast.NodeId) Value ! E
|
||||
return Value{}
|
||||
}
|
||||
value :: outcome.value
|
||||
source_type :: typecheck.type_of(&checked, expression)
|
||||
source_type :: typecheck.type_of(&checked, result_type)
|
||||
node :: try serialize_value(state, &module, &checked, source_type, value, span)
|
||||
if (node == no_node) {
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
|
||||
return Value{}
|
||||
}
|
||||
if (ir.aggregate(value.type) or source_type.kind == .enumeration)
|
||||
if (ir.aggregate(value.type) or source_type.kind == .enumeration or source_type.kind == .void)
|
||||
return Value{ kind = .aggregate, node = node, annotation = result_type }
|
||||
result := try evaluate(state, node)
|
||||
result.annotation = result_type
|
||||
@@ -1405,7 +1433,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
|
||||
match kind {
|
||||
.typed_constant: {
|
||||
view :: ast.typed_constant(&state.tree, id)
|
||||
value := try evaluate(state, view.value)
|
||||
value := try evaluate_expected(state, view.value, view.type)
|
||||
value.annotation = view.type
|
||||
return value
|
||||
}
|
||||
@@ -1439,6 +1467,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
|
||||
return Value{ kind = .string, node = id }
|
||||
}
|
||||
.literal_null: return Value{ kind = .null, node = id }
|
||||
.literal_void: return Value{ kind = .aggregate, node = id, annotation = try builtin(state, id, .void) }
|
||||
.record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type,
|
||||
.enum_type, .union_type, .distinct_type, .fallible_type, .sum_type:
|
||||
return Value{ kind = .type, node = id }
|
||||
@@ -1458,7 +1487,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
|
||||
}
|
||||
.binary: return try evaluate_binary(state, id)
|
||||
.call: return try evaluate_call(state, id)
|
||||
.record_literal, .array_literal, .selection, .index, .match_expr, .builtin_call:
|
||||
.record_literal, .array_literal, .selection, .index, .match_expr, .builtin_call, .catch_expr:
|
||||
return try evaluate_ir(state, id, try expression_type(state, id, 0))
|
||||
else: return Value{}
|
||||
}
|
||||
@@ -1759,6 +1788,12 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
|
||||
.literal_float: return try builtin(state, id, .f64)
|
||||
.literal_bool: return try builtin(state, id, .bool)
|
||||
.literal_string: return try builtin(state, id, .string)
|
||||
.literal_void: return try builtin(state, id, .void)
|
||||
.catch_expr: {
|
||||
type :: try expression_type(state, ast.catch_expr(&state.tree, id).operand, depth + 1)
|
||||
if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node
|
||||
return try canonical_type(state, ast.binary_expr(&state.tree, type).left)
|
||||
}
|
||||
.record_literal: {
|
||||
view :: ast.record_literal(&state.tree, id)
|
||||
if (view.type) |annotation| return try canonical_type(state, annotation)
|
||||
@@ -2001,6 +2036,10 @@ valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Er
|
||||
if (kind == .bool) return value.kind == .boolean
|
||||
if (kind == .string) return value.kind == .string
|
||||
if (kind == .float or kind == .f64) return value.kind == .real
|
||||
if (kind == .void) return (
|
||||
value.kind == .aggregate and value.annotation != no_node
|
||||
and try same_type(state, type, value.annotation)
|
||||
)
|
||||
if (value.kind != .integer) return false
|
||||
bits := u32(64)
|
||||
signed := false
|
||||
|
||||
+200
@@ -817,6 +817,206 @@ CASES = {
|
||||
main proc() usize { return $(make())[0].len }
|
||||
""", b"", b"compile-time result cannot escape evaluation",
|
||||
),
|
||||
"comptime_tagged_union_preserves_tags_and_nested_raw_payloads": (
|
||||
"""
|
||||
Tag :: enum(i16) { number = -7, empty = 12, zero = 41, payload = 90 }
|
||||
Empty :: struct {}
|
||||
Payload :: struct { rows [2]struct { values [2]i16 }, text string }
|
||||
Choice :: union(Tag) { payload Payload, empty void, zero Empty, number i16 }
|
||||
make proc() Choice {
|
||||
return .payload { rows = [{ values = [10, 20] }, { values = [4, 6] }], text = "ok" }
|
||||
}
|
||||
numeric proc() Choice { return .number{42} }
|
||||
empty proc() Choice { return .empty }
|
||||
zero proc() Choice { return .zero{} }
|
||||
score proc(value Choice) i16 {
|
||||
return match value {
|
||||
.number |number|: number,
|
||||
.payload |payload|: (
|
||||
payload.rows[0].values[0] + payload.rows[0].values[1]
|
||||
+ payload.rows[1].values[0] + payload.rows[1].values[1] + i16(payload.text.len)
|
||||
),
|
||||
.empty: 3,
|
||||
.zero: 5,
|
||||
}
|
||||
}
|
||||
main proc() [5]i16 {
|
||||
value :: $(make())
|
||||
return [score(value), value.payload.rows[1].values[1], score($(numeric())),
|
||||
score($(empty())), score($(zero()))]
|
||||
}
|
||||
""", b"[42, 6, 42, 3, 5]\n", None,
|
||||
),
|
||||
"comptime_tagged_union_value_parameters_and_cache_identity": (
|
||||
"""
|
||||
Choice :: union(enum) { left i16, right i16 }
|
||||
make proc() Choice { return .left{2} }
|
||||
Fixed proc($choice Choice) type {
|
||||
return struct { values [usize(choice.left)]i16 }
|
||||
}
|
||||
Token proc($choice Choice) type { return struct {} }
|
||||
score proc($choice Choice) i16 {
|
||||
return match choice { .left |value|: value, .right |value|: value + 1 }
|
||||
}
|
||||
main proc() i16 {
|
||||
first Fixed(make()) :: { values = [20, 20] }
|
||||
same Fixed(Choice { left = 2 }) :: first
|
||||
if (Token(make()) != Token(Choice { left = 2 })) return 0
|
||||
if (Token(make()) == Token(Choice { right = 2 })) return 0
|
||||
if (Token(make()) == Token(Choice { left = 3 })) return 0
|
||||
return same.values[0] + same.values[1] + score(make())
|
||||
}
|
||||
""", b"42\n", None,
|
||||
),
|
||||
"comptime_fallible_nested_storage_preserves_both_channels": (
|
||||
"""
|
||||
Error :: enum { failed }
|
||||
Storage :: struct { values [2](i16 ! Error), text string ! Error }
|
||||
make proc() Storage { return { values = [40, .failed], text = "ok" } }
|
||||
main proc() i16 {
|
||||
storage :: $(make())
|
||||
text :: storage.text catch ""
|
||||
return (storage.values[0] catch 0) + (storage.values[1] catch i16(text.len))
|
||||
}
|
||||
""", b"42\n", None,
|
||||
),
|
||||
"comptime_fallible_materializes_composed_and_payload_errors": (
|
||||
"""
|
||||
Missing :: enum { missing }
|
||||
Detail :: union(enum) { failed struct { rows [2]i16, text string }, empty void }
|
||||
Failure :: struct { code i16 }
|
||||
missing proc() i16 ! (Missing | Detail) { return .missing }
|
||||
detail proc() i16 ! (Missing | Detail) {
|
||||
return .failed { rows = [20, 20], text = "ok" }
|
||||
}
|
||||
record proc() i16 ! Failure { return Failure { code = 42 } }
|
||||
read proc(value i16 ! (Missing | Detail)) i16 {
|
||||
return value catch |err| match err {
|
||||
.missing: 7,
|
||||
.failed |payload|: payload.rows[0] + payload.rows[1] + i16(payload.text.len),
|
||||
.empty: 0,
|
||||
}
|
||||
}
|
||||
main proc() [3]i16 {
|
||||
return [read($(missing())), read($(detail())), $(record()) catch |err| err.code]
|
||||
}
|
||||
""", b"[7, 42, 42]\n", None,
|
||||
),
|
||||
"comptime_fallible_value_parameters_type_inputs_and_cache_identity": (
|
||||
"""
|
||||
Error :: enum { failed, other }
|
||||
good proc() i16 ! Error { return 2 }
|
||||
bad proc() i16 ! Error { return .failed }
|
||||
Fixed proc($result i16 ! Error) type {
|
||||
return struct { values [usize(result catch 1)]i16 }
|
||||
}
|
||||
Token proc($result i16 ! Error) type { return struct {} }
|
||||
score proc($result i16 ! Error) i16 { return result catch 40 }
|
||||
main proc() i16 {
|
||||
first Fixed(good()) :: { values = [20, 20] }
|
||||
same Fixed(2) :: first
|
||||
if (Token(good()) != Token(2) or Token(bad()) != Token(Error.failed)) return 0
|
||||
if (Token(good()) == Token(3) or Token(bad()) == Token(Error.other)) return 0
|
||||
if (Token(good()) == Token(bad())) return 0
|
||||
return same.values[0] + same.values[1] + score(good())
|
||||
}
|
||||
""", b"42\n", None,
|
||||
),
|
||||
"comptime_fallible_void_success_and_error_channels": (
|
||||
"""
|
||||
Error :: enum { failed }
|
||||
good proc() void ! Error {}
|
||||
bad proc() void ! Error { return .failed }
|
||||
score proc($result void ! Error) i16 {
|
||||
result catch { return 2 }
|
||||
return 40
|
||||
}
|
||||
main proc() i16 {
|
||||
value i16 := 0
|
||||
$(good()) catch { value = 100 }
|
||||
$(bad()) catch { value = 2 }
|
||||
return value + score(good()) + score(bad()) - 2
|
||||
}
|
||||
""", b"42\n", None,
|
||||
),
|
||||
"comptime_fallible_preserves_overlapping_channel_types": (
|
||||
"""
|
||||
Success :: enum { failed }
|
||||
Error :: enum { failed }
|
||||
Value :: struct { code i16 }
|
||||
Failure :: struct { code i16 }
|
||||
good proc() Success ! Error { return Success.failed }
|
||||
bad proc() Success ! Error { return Error.failed }
|
||||
good_record proc() Value ! Failure { return Value { code = 40 } }
|
||||
bad_record proc() Value ! Failure { return Failure { code = 2 } }
|
||||
enum_score proc() i16 {
|
||||
value :: $(good()) catch { return -1 }
|
||||
if (value != Success.failed) return -2
|
||||
$(bad()) catch { return 42 }
|
||||
return 0
|
||||
}
|
||||
main proc() [2]i16 {
|
||||
left :: $(good_record()) catch Value { code = 0 }
|
||||
right :: $(bad_record()) catch |err| Value { code = err.code }
|
||||
return [enum_score(), left.code + right.code]
|
||||
}
|
||||
""", b"[42, 42]\n", None,
|
||||
),
|
||||
"comptime_tagged_union_projects_raw_nested_channels": (
|
||||
"""
|
||||
Error :: enum { failed }
|
||||
Choice :: union(enum) {
|
||||
data struct { maybe ?i16, result i16 ! Error }
|
||||
pointer @i16
|
||||
empty void
|
||||
}
|
||||
make proc() Choice { return .data { maybe = 40, result = .failed } }
|
||||
absent proc() Choice { return .empty }
|
||||
main proc() i16 {
|
||||
value :: $(make())
|
||||
empty :: $(absent())
|
||||
match empty { .empty: {}, else: return 0 }
|
||||
return value.data.maybe? + (value.data.result catch 2)
|
||||
}
|
||||
""", b"42\n", None,
|
||||
),
|
||||
"comptime_tagged_union_rejects_nonnull_pointer_payload_escape": (
|
||||
"""
|
||||
Choice :: union(enum) { pointer @i16, empty void }
|
||||
make proc() Choice {
|
||||
value i16 :: 42
|
||||
return .pointer{&value}
|
||||
}
|
||||
main proc() i16 { return $(make()).pointer^ }
|
||||
""", b"", b"compile-time result cannot escape evaluation",
|
||||
),
|
||||
"comptime_fallible_rejects_nonnull_success_pointer_escape": (
|
||||
"""
|
||||
Error :: enum { failed }
|
||||
make proc() @i16 ! Error {
|
||||
value i16 :: 42
|
||||
return &value
|
||||
}
|
||||
main proc() i16 { return ($(make()) catch null)^ }
|
||||
""", b"", b"compile-time result cannot escape evaluation",
|
||||
),
|
||||
"comptime_fallible_rejects_nonnull_error_pointer_escape": (
|
||||
"""
|
||||
Failure :: struct { pointer @i16 }
|
||||
make proc() i16 ! Failure {
|
||||
value i16 :: 42
|
||||
return Failure { pointer = &value }
|
||||
}
|
||||
main proc() i16 { return $(make()) catch |err| err.pointer^ }
|
||||
""", b"", b"compile-time result cannot escape evaluation",
|
||||
),
|
||||
"comptime_untagged_union_remains_an_escape_trap": (
|
||||
"""
|
||||
Storage :: union { number i16, bits u16 }
|
||||
make proc() Storage { return Storage { number = 42 } }
|
||||
main proc() i16 { return $(make()).number }
|
||||
""", b"", b"compile-time result cannot escape evaluation",
|
||||
),
|
||||
"comptime_float_bits_survive_aggregate_materialization": (
|
||||
"""
|
||||
Floats :: struct { zero f64, nan f64 }
|
||||
|
||||
Reference in New Issue
Block a user