feat: infer compile-time metadata locals

This commit is contained in:
2026-10-31 10:50:37 +01:00
parent 4fea18b834
commit e036070bf2
7 changed files with 419 additions and 78 deletions
+2 -2
View File
@@ -35,7 +35,7 @@ main proc() int {
} }
``` ```
this prints `42`. both interpreted and Honey-compiled `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. this prints `42`. both interpreted and Honey-compiled `main` must be a top-level runtime function with no parameters; compile-time-only procedures cannot be entry points. nonvoid results are printed, not used as process exit statuses.
compile the same supported language subset to a standalone Apple Silicon macOS executable: compile the same supported language subset to a standalone Apple Silicon macOS executable:
@@ -134,7 +134,7 @@ procedures returning fixed arrays or records/tuples containing `type`, such as `
ordinary parameters containing `type` also make a procedure compile-time-only, even when its result is a runtime value. for example, `count proc(types []type) usize { return types.len }` computes `count([i16, bool])` without runtime metadata. every input must be explicitly supplied and constant; use `$` parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked `try`/`catch` with scoped cleanup. plain and fallible `void` support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete metadata arguments remain diagnostic; mutable metadata borrowing and general type-valued execution remain open. ordinary parameters containing `type` also make a procedure compile-time-only, even when its result is a runtime value. for example, `count proc(types []type) usize { return types.len }` computes `count([i16, bool])` without runtime metadata. every input must be explicitly supplied and constant; use `$` parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked `try`/`catch` with scoped cleanup. plain and fallible `void` support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete metadata arguments remain diagnostic; mutable metadata borrowing and general type-valued execution remain open.
explicit local annotations containing `type`, such as `T type := i16`, also make a procedure compile-time-only, even with ordinary scalar inputs and results. calls evaluate with owned local metadata and are erased. nested metadata records/arrays, partial initialization, checked updates, loops, recursion and scoped cleanup reuse type-control execution and its shared 4096-step bound. every input remains explicitly supplied and constant; this is not partial evaluation of runtime inputs. ordinary local type aliases without metadata storage annotations keep their runtime-call contract. inferred metadata-only locals and mutable metadata borrowing remain open. local metadata storage also makes a procedure compile-time-only, even with ordinary scalar inputs and results. explicit annotations (`T type := i16`) and inferred mutable type bindings (`T := i16`), metadata array/record/tuple literals, helper results, checked projections and yielding blocks use owned snapshots and erased calls. nested storage, partial initialization, checked updates, loops, recursion and scoped cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their type explicitly permits element updates; whole-value replacement remains available for mutable bindings. every input remains explicitly supplied and constant; this is not partial evaluation of runtime inputs. ordinary local `alias` declarations and scalar projections from metadata helpers keep their runtime-call contract. mixed runtime/type-valued execution and mutable metadata borrowing remain open.
readonly type-valued slices (`[]type` and slices of metadata records/arrays) also cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them; explicit views of immutable readonly arrays and existing metadata slices support checked `usize` bounds, empty end views, indexing and `.len`. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. snapshots generate runtime types/defaults, but metadata itself has no runtime storage. mutable slice types, sentinels and views of mutable array places remain rejected rather than silently losing borrowing semantics; copy an array into an immutable binding before taking an owned view. construction shares the type-control step bound, and uninitialized leaves cannot cross procedure boundaries. readonly type-valued slices (`[]type` and slices of metadata records/arrays) also cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them; explicit views of immutable readonly arrays and existing metadata slices support checked `usize` bounds, empty end views, indexing and `.len`. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. snapshots generate runtime types/defaults, but metadata itself has no runtime storage. mutable slice types, sentinels and views of mutable array places remain rejected rather than silently losing borrowing semantics; copy an array into an immutable binding before taking an owned view. construction shares the type-control step bound, and uninitialized leaves cannot cross procedure boundaries.
+3 -2
View File
@@ -58,7 +58,7 @@ checked immutable-array/slice views, by-value iteration and erased arguments;
mutable metadata borrowing remains open. mutable metadata borrowing remains open.
ordinary metadata-input procedures also compute owned runtime values, including ordinary metadata-input procedures also compute owned runtime values, including
reflected field names/counts, recursive helpers and fallible cleanup. reflected field names/counts, recursive helpers and fallible cleanup.
explicitly annotated local metadata storage also makes ordinary procedures explicit and inferred local metadata storage also make ordinary procedures
constant-input producers, preserving owned updates, loops, recursion and cleanup. constant-input producers, preserving owned updates, loops, recursion and cleanup.
`field!` selects record/tuple/enum and sequence members using compile-time names, `field!` selects record/tuple/enum and sequence members using compile-time names,
including runtime places and type-control updates without runtime reflection. including runtime places and type-control updates without runtime reflection.
@@ -174,7 +174,8 @@ test discovery and FFI remain outside these completed slices.
- [x] readonly type-slice checkpoint: `[]type` and slices of type-valued records/arrays use owned compile-time snapshots. contextual literals, immutable readonly array views, nested slice views, checked `usize` bounds/indexing, `.len`, empty shapes and ordinary/inline by-value iteration preserve exact nominal leaves. constant-input procedure parameters/results, erased generic inputs, optional/fallible nesting and normalized cache keys generate runtime storage/defaults through interpreter/native execution. mutable annotations/backing, metadata sentinels, invalid ranges/indices, readonly writes, incomplete leaves and runtime dependencies retain diagnostics. imported private types, source/session disposal, shared construction bounds and repeat compilation preserve ownership. mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open. - [x] readonly type-slice checkpoint: `[]type` and slices of type-valued records/arrays use owned compile-time snapshots. contextual literals, immutable readonly array views, nested slice views, checked `usize` bounds/indexing, `.len`, empty shapes and ordinary/inline by-value iteration preserve exact nominal leaves. constant-input procedure parameters/results, erased generic inputs, optional/fallible nesting and normalized cache keys generate runtime storage/defaults through interpreter/native execution. mutable annotations/backing, metadata sentinels, invalid ranges/indices, readonly writes, incomplete leaves and runtime dependencies retain diagnostics. imported private types, source/session disposal, shared construction bounds and repeat compilation preserve ownership. mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open.
- [x] tagged type-storage checkpoint: tagged unions containing type leaves own active payload snapshots through typed/contextual construction, direct and nested field/index updates, immutable by-value matches and whole-value variant replacement. constant-input procedure arguments/results, erased generic inputs and normalized cache keys preserve nominal leaves and saved copies while generating runtime storage/defaults through interpreter/native execution. undefined union locals and nested record/array storage require whole-value initialization; inactive reads/writes, malformed explicit tags, nominal initializer conflicts, readonly updates, incomplete results and narrow overflow retain diagnostics. explicit sparse tag enums may contain unused members without adding match cases. imported private types, source/session disposal and repeat compilation preserve ownership. untagged metadata layouts remain invalid; mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open. - [x] tagged type-storage checkpoint: tagged unions containing type leaves own active payload snapshots through typed/contextual construction, direct and nested field/index updates, immutable by-value matches and whole-value variant replacement. constant-input procedure arguments/results, erased generic inputs and normalized cache keys preserve nominal leaves and saved copies while generating runtime storage/defaults through interpreter/native execution. undefined union locals and nested record/array storage require whole-value initialization; inactive reads/writes, malformed explicit tags, nominal initializer conflicts, readonly updates, incomplete results and narrow overflow retain diagnostics. explicit sparse tag enums may contain unused members without adding match cases. imported private types, source/session disposal and repeat compilation preserve ownership. untagged metadata layouts remain invalid; mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open.
- [x] metadata-input execution checkpoint: procedures with ordinary type-valued parameters execute entirely at compile time even when returning runtime-representable values. all inputs remain explicit constants; owned scalar/record/array/string results, recursive readonly metadata iteration, reflected field names/counts and private nominal leaves work through interpreter/native execution. plain/fallible `void` retain bare-return/fallthrough semantics; ordinary fallible results propagate through `try`/`catch` and run scoped `defer`/`errdefer`. runtime dependencies and incomplete payloads retain diagnostics; source/session disposal and repeat compilation preserve ownership. dependent signature parameters still require `$`; general type-valued execution, mutable metadata borrowing and broader constant materialization remain open. - [x] metadata-input execution checkpoint: procedures with ordinary type-valued parameters execute entirely at compile time even when returning runtime-representable values. all inputs remain explicit constants; owned scalar/record/array/string results, recursive readonly metadata iteration, reflected field names/counts and private nominal leaves work through interpreter/native execution. plain/fallible `void` retain bare-return/fallthrough semantics; ordinary fallible results propagate through `try`/`catch` and run scoped `defer`/`errdefer`. runtime dependencies and incomplete payloads retain diagnostics; source/session disposal and repeat compilation preserve ownership. dependent signature parameters still require `$`; general type-valued execution, mutable metadata borrowing and broader constant materialization remain open.
- [x] explicit metadata-local execution checkpoint: procedures with explicitly annotated type-valued locals execute entirely at compile time without metadata inputs or results. scalar inputs remain explicit constants; nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse existing type-control execution through interpreter/native paths. runtime inputs and uninitialized leaves diagnose; ordinary unannotated local type aliases retain runtime parameters. imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. inferred metadata-only locals, mixed runtime/type-valued execution, mutable metadata borrowing and broader constant materialization remain open. - [x] explicit metadata-local execution checkpoint: procedures with explicitly annotated type-valued locals execute entirely at compile time without metadata inputs or results. scalar inputs remain explicit constants; nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse existing type-control execution through interpreter/native paths. runtime inputs and uninitialized leaves diagnose; ordinary unannotated local type aliases retain runtime parameters. imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. inferred metadata-only locals are covered below; mixed runtime/type-valued execution, mutable metadata borrowing and broader constant materialization remain open.
- [x] inferred metadata-local execution checkpoint: inferred mutable type bindings and metadata array/record/tuple locals select fully constant-input execution without metadata inputs, results or local annotations. literal/helper results, checked field/index/optional projections, yielding blocks and conditional/fallible selection preserve owned copies, nominal leaves, whole-value replacement, loops, recursion and scoped cleanup through interpreter/native execution. inferred array elements remain readonly; runtime inputs diagnose. ordinary local aliases and scalar metadata projections retain runtime parameters. ordinary fallible success/error snapshots use checked channel discrimination rather than assuming every payload carries a nominal type. imported private types, source/session disposal and repeat compilation preserve ownership. erased compile-time-only procedures cannot become runtime entries. mixed runtime/type-valued execution, mutable metadata borrowing and broader materialization remain open.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates. - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. 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. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
+1 -1
View File
@@ -1470,7 +1470,7 @@ tagged_type_metadata_recompiles_owned_private_nominal_payloads test {
` return i16(saved.payload.count) + bias ` return i16(saved.payload.count) + bias
`} `}
`score_local proc(bias i16) i16 { `score_local proc(bias i16) i16 {
` config Config := { children = [Child, null], count = 2 } ` config := Config { children = [Child, null], count = 2 }
` saved :: config ` saved :: config
` config.children[0] = bool ` config.children[0] = bool
` if (saved.children[0]? != Child) return -1 ` if (saved.children[0]? != Child) return -1
+1 -1
View File
@@ -238,7 +238,7 @@ messages :: $enummap.init(Code, []u8, {
recursive_record = "record has a recursive by-value layout", recursive_record = "record has a recursive by-value layout",
invalid_record_field_type = "record fields require a concrete value type", invalid_record_field_type = "record fields require a concrete value type",
missing_entry = "program requires a main function", missing_entry = "program requires a main function",
invalid_entry = "main must be a function with no parameters", invalid_entry = "main must be a runtime function with no parameters",
literal_out_of_range = "numeric literal is outside its runtime type's range", literal_out_of_range = "numeric literal is outside its runtime type's range",
inexact_integer_literal = "integer literal is not exactly representable as float; use a floating literal to allow rounding", inexact_integer_literal = "integer literal is not exactly representable as float; use a floating literal to allow rounding",
invalid_escape = "invalid string escape", invalid_escape = "invalid string escape",
+1 -1
View File
@@ -393,7 +393,7 @@ find_entry proc(state @mut State, root_package ast.NodeId) void ! (mem.AllocErro
.import, .invalid: continue .import, .invalid: continue
} }
if (!mem.eql(ast.source_text(tree, name), "main")) continue if (!mem.eql(ast.source_text(tree, name), "main")) continue
if (ast.decl_kind(tree, id) != .function) { if (ast.decl_kind(tree, id) != .function or state.storage[usize(id)].kind != .function) {
state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name) state.module.entry_diagnostic = try diagnostics.add(state.diagnostics, .invalid_entry, name)
return return
} }
+236 -49
View File
@@ -664,7 +664,7 @@ expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
) { ) {
for (0..ast.record_field_count(&state.tree, id)) |i| { for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i)) field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
slot :: if (field.name != null) type_record_slot(state, shape, field.name?) else i slot :: if (field.name != null) type_record_slot(&state.tree, shape, field.name?) else i
field_type := no_node field_type := no_node
if (slot < ast.record_field_count(&state.tree, shape)) { if (slot < ast.record_field_count(&state.tree, shape)) {
if (ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type) |type| if (ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type) |type|
@@ -1688,7 +1688,7 @@ specialize_match proc(state @mut State, id, statement, expected ast.NodeId) bool
if (ast.expr_kind(&state.tree, annotation) != .union_type) break if (ast.expr_kind(&state.tree, annotation) != .union_type) break
pattern :: ast.match_pattern(&state.tree, arm_id, 0) pattern :: ast.match_pattern(&state.tree, arm_id, 0)
if (ast.expr_kind(&state.tree, pattern) != .enum_literal) continue if (ast.expr_kind(&state.tree, pattern) != .enum_literal) continue
slot :: type_record_slot(state, annotation, ast.enum_literal(&state.tree, pattern)) slot :: type_record_slot(&state.tree, annotation, ast.enum_literal(&state.tree, pattern))
if (slot == no_index) continue if (slot == no_index) continue
field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot)) field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot))
if (field.type) |type| payload = type if (field.type) |type| payload = type
@@ -1860,8 +1860,181 @@ type_storage_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast.
return false return false
} }
# explicit metadata locals have no runtime storage, just like metadata parameters. @hide:file
# do not classify ordinary local type aliases as constant-input procedures. storage_schema proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize) ast.NodeId {
if (depth >= 128 or usize(input) >= bindings.len) return no_node
resolved :: bindings[usize(input)]
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
if (ast.node_class(tree, target) == .function) return target
if (ast.node_class(tree, target) == .statement)
return storage_schema(tree, bindings, ast.statement_decl(tree, target).initializer, depth + 1)
}
if (ast.expr_kind(tree, input) == .alias_expr)
return storage_schema(tree, bindings, ast.type_operand(tree, input), depth + 1)
return input
}
# read only existing annotations: discovery must not specialize recursive calls.
@hide:file
storage_expression_annotation proc(
tree @ast.Tree,
bindings []sema.Binding,
input ast.NodeId,
depth usize,
) ast.NodeId {
if (depth >= 128 or usize(input) >= bindings.len) return no_node
resolved :: bindings[usize(input)]
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
class :: ast.node_class(tree, target)
if (class == .parameter) return ast.param(tree, target).type
if (class == .statement) {
declaration :: ast.statement_decl(tree, target)
if (declaration.type) |annotation| return annotation
return storage_expression_annotation(tree, bindings, declaration.initializer, depth + 1)
}
return no_node
}
kind :: ast.expr_kind(tree, input)
if (kind == .typed_constant) return ast.typed_constant(tree, input).type
if (kind == .union_constant) return ast.union_constant(tree, input).type
if (kind == .slice_constant) return ast.slice_constant(tree, input).type
if (kind == .record_literal) {
if (ast.record_literal(tree, input).type) |type| return type
return no_node
}
if (kind == .call) {
callee :: storage_schema(tree, bindings, ast.call_expr(tree, input).callee, depth + 1)
if (callee != no_node and ast.node_class(tree, callee) == .function)
return ast.function_decl(tree, callee).return_type
return no_node
}
base := no_node
if (kind == .selection) base = ast.selection_expr(tree, input).base
else if (kind == .index) base = ast.index_expr(tree, input).base
else if (kind == .slice) base = ast.slice_expr(tree, input).base
else if (kind == .unary) base = ast.unary_expr(tree, input).operand
else if (kind == .try_expr) base = ast.type_operand(tree, input)
else if (kind == .catch_expr) base = ast.catch_expr(tree, input).operand
else if (kind == .comptime_expr) return storage_expression_annotation(tree, bindings,
ast.comptime_operand(tree, input), depth + 1)
if (base == no_node) return no_node
annotation :: storage_schema(tree, bindings,
storage_expression_annotation(tree, bindings, base, depth + 1), depth + 1)
if (annotation == no_node) return no_node
shape :: ast.expr_kind(tree, annotation)
if (kind == .slice) return annotation
if (kind == .try_expr or kind == .catch_expr)
return if (shape == .fallible_type) ast.binary_expr(tree, annotation).left else no_node
if (kind == .unary)
return if (shape == .optional_type and ast.unary_expr(tree, input).operator == .unwrap)
ast.type_operand(tree, annotation) else no_node
if (kind == .index) {
if (shape == .array_type) return ast.array_type(tree, annotation).element
if (shape == .slice_type) return ast.slice_type(tree, annotation).element
return no_node
}
if (shape != .record_type and shape != .union_type) return no_node
member :: ast.selection_expr(tree, input).member
slot :: type_record_slot(tree, annotation, member)
if (slot == no_index) return no_node
if (ast.field(tree, ast.record_field(tree, annotation, slot)).type) |type| return type
return no_node
}
@hide:file
type_storage_yields proc(
tree @ast.Tree,
bindings []sema.Binding,
id ast.NodeId,
scalar_type bool,
depth usize,
) bool {
if (depth >= 128) return false
class :: ast.node_class(tree, id)
if (class != .block and class != .statement) return false
if (class == .statement and ast.stmt_kind(tree, id) == .yield_stmt) {
value :: ast.yield_stmt(tree, id).value
return value != null and type_storage_initializer(tree, bindings, value?, scalar_type, depth + 1)
}
for (0..ast.child_count(tree, id)) |i| {
if (type_storage_yields(tree, bindings, ast.child(tree, id, i), scalar_type, depth + 1)) return true
}
return false
}
# discover inferred storage without elaborating calls or executing initializers.
# immutable scalar type bindings remain aliases in ordinary runtime procedures.
@hide:file
type_storage_initializer proc(
tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, scalar_type bool, depth usize,
) bool {
if (depth >= 128 or usize(input) >= bindings.len) return false
annotation :: storage_schema(tree, bindings,
storage_expression_annotation(tree, bindings, input, depth + 1), depth + 1)
if (annotation != no_node) {
if (!scalar_type and bindings[usize(annotation)].kind == .builtin
and bindings[usize(annotation)].data.builtin == .type) return false
return type_storage_annotation(tree, bindings, annotation, depth + 1)
}
resolved :: bindings[usize(input)]
if (resolved.kind == .builtin) return scalar_type
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
class :: ast.node_class(tree, target)
if (class == .statement) {
declaration :: ast.statement_decl(tree, target)
if (declaration.type != null) return false
return type_storage_initializer(tree, bindings, declaration.initializer, scalar_type, depth + 1)
}
return false
}
kind :: ast.expr_kind(tree, input)
match kind {
.record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type,
.enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr:
return scalar_type
.record_literal: {
for (0..ast.record_field_count(tree, input)) |i| {
field :: ast.field(tree, ast.record_field(tree, input, i))
if (field.value != null
and type_storage_initializer(tree, bindings, field.value?, true, depth + 1)) return true
}
return false
}
.array_literal: {
for (0..ast.array_element_count(tree, input)) |i| {
if (type_storage_initializer(tree, bindings, ast.array_element(tree, input, i), true, depth + 1))
return true
}
return false
}
.comptime_expr:
return type_storage_initializer(tree, bindings, ast.comptime_operand(tree, input), scalar_type, depth + 1)
.match_expr: {
for (0..ast.match_arm_count(tree, input)) |i| {
arm :: ast.match_arm_view(tree, ast.match_arm(tree, input, i))
if (arm.expression) {
if (type_storage_initializer(tree, bindings, arm.body, scalar_type, depth + 1)) return true
} else if (type_storage_yields(tree, bindings, arm.body, scalar_type, depth + 1)) return true
}
return false
}
.block_expr:
return type_storage_yields(tree, bindings, ast.block_expr(tree, input), scalar_type, depth + 1)
.builtin_call: {
name :: ast.literal_text(tree, ast.builtin_call(tree, input).name)
if (mem.eql(name, "typeinfo")) return true
if (mem.eql(name, "struct_type")) return scalar_type
if (mem.eql(name, "some") and ast.call_arg_count(tree, input) == 1)
return type_storage_initializer(tree, bindings, ast.call_arg(tree, input, 0), true, depth + 1)
return false
}
else: return false
}
}
@hide:file @hide:file
type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId, depth usize) bool { type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId, depth usize) bool {
if (depth >= 128) return false if (depth >= 128) return false
@@ -1870,6 +2043,8 @@ type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId,
if (declaration.type) |annotation| { if (declaration.type) |annotation| {
if (type_storage_annotation(tree, bindings, annotation, 0)) return true if (type_storage_annotation(tree, bindings, annotation, 0)) return true
} }
else if (type_storage_initializer(tree, bindings, declaration.initializer,
declaration.mutability == .variable, 0)) return true
} }
for (0..ast.child_count(tree, id)) |i| { for (0..ast.child_count(tree, id)) |i| {
if (type_local_storage(tree, bindings, ast.child(tree, id, i), depth + 1)) return true if (type_local_storage(tree, bindings, ast.child(tree, id, i), depth + 1)) return true
@@ -1939,7 +2114,7 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
state.depth += 1 state.depth += 1
defer state.depth -= 1 defer state.depth -= 1
count :: ast.function_param_count(&state.tree, function) count :: ast.function_param_count(&state.tree, function)
# metadata inputs, results and explicit locals require a fully constant call. # metadata inputs, results and locals require a fully constant call.
type_result :: compiletime_call(state, function) type_result :: compiletime_call(state, function)
runtime := usize(0) runtime := usize(0)
for (0..count) |i| { for (0..count) |i| {
@@ -3332,26 +3507,19 @@ type_snapshot_expected proc(
} }
@hide:file @hide:file
type_snapshot_catch proc( type_fallible_failed proc(
state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, state @mut State,
) ast.NodeId ! Error { owned, type, at ast.NodeId,
view :: ast.catch_expr(&state.tree, id) ) ?bool ! Error {
operand :: try type_snapshot(state, execution, view.operand) missing ?bool :: null
if (operand == no_node) return no_node if (type_record_storage(state, type, 0)) {
try expand_expected(state, operand, annotation) if (!try type_array_initialized(state, owned, at)) return missing
operand_type :: try expression_type(state, operand, 0) return !try same_type(state,
if (operand_type == no_node or ast.expr_kind(&state.tree, operand_type) != .fallible_type) return no_node ast.typed_constant(&state.tree, ast.typed_constant(&state.tree, owned).value).type,
channels :: ast.binary_expr(&state.tree, operand_type) ast.binary_expr(&state.tree, type).left)
owned :: try type_execution_value(state, operand, operand_type) }
if (owned == no_node) return no_node span :: ast.node_span(&state.tree, at)
span :: ast.node_span(&state.tree, id) boolean :: try builtin(state, at, .bool)
failed := false
if (type_record_storage(state, operand_type, 0)) {
if (!try type_array_initialized(state, owned, id)) return no_node
failed = !try same_type(state,
ast.typed_constant(&state.tree, ast.typed_constant(&state.tree, owned).value).type, channels.left)
} else {
boolean :: try builtin(state, id, .bool)
yes :: try ast.add_boolean(state.builder, span, true) yes :: try ast.add_boolean(state.builder, span, true)
no :: try ast.add_boolean(state.builder, span, false) no :: try ast.add_boolean(state.builder, span, false)
returned :: try ast.add_return_stmt(state.builder, span, yes) returned :: try ast.add_return_stmt(state.builder, span, yes)
@@ -3363,10 +3531,29 @@ type_snapshot_catch proc(
statements [2]ast.NodeId :: [discarded, success] statements [2]ast.NodeId :: [discarded, success]
body :: try ast.add_block(state.builder, span, statements[..]) body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state) try refresh(state)
guard :: try evaluate_ir_body(state, id, body, boolean) guard :: try evaluate_ir_body(state, at, body, boolean)
if (guard.kind != .boolean) return no_node if (guard.kind != .boolean) return missing
failed = guard.boolean return guard.boolean
} }
@hide:file
type_snapshot_catch proc(
state @mut State,
execution @mut TypeExecution,
id, annotation ast.NodeId,
) ast.NodeId ! Error {
view :: ast.catch_expr(&state.tree, id)
operand :: try type_snapshot(state, execution, view.operand)
if (operand == no_node) return no_node
try expand_expected(state, operand, annotation)
operand_type :: try expression_type(state, operand, 0)
if (operand_type == no_node or ast.expr_kind(&state.tree, operand_type) != .fallible_type) return no_node
channels :: ast.binary_expr(&state.tree, operand_type)
owned :: try type_execution_value(state, operand, operand_type)
if (owned == no_node) return no_node
failure :: try type_fallible_failed(state, owned, operand_type, id)
if (failure == null) return no_node
failed :: failure?
payload :: ast.typed_constant(&state.tree, owned).value payload :: ast.typed_constant(&state.tree, owned).value
if (!failed) return try type_execution_value(state, payload, channels.left) if (!failed) return try type_execution_value(state, payload, channels.left)
saved :: execution.locals.items.len saved :: execution.locals.items.len
@@ -3791,7 +3978,7 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
continue continue
} }
shape :: try inline_match_shape(state, annotation) shape :: try inline_match_shape(state, annotation)
slot :: type_record_slot(state, shape, ast.enum_literal(&state.tree, pattern)) slot :: type_record_slot(&state.tree, shape, ast.enum_literal(&state.tree, pattern))
if (slot == no_index) { if (slot == no_index) {
valid = false valid = false
continue continue
@@ -4002,7 +4189,7 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error {
if (record) { if (record) {
if (kind != .selection) return true if (kind != .selection) return true
member :: ast.selection_expr(&state.tree, id).member member :: ast.selection_expr(&state.tree, id).member
slot :: type_record_slot(state, type, member) slot :: type_record_slot(&state.tree, type, member)
if (slot == no_index) { if (slot == no_index) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unknown_field, ast.node_span(&state.tree, member)) .unknown_field, ast.node_span(&state.tree, member))
@@ -4087,24 +4274,24 @@ type_record_storage proc(state @State, annotation ast.NodeId, depth usize) bool
} }
@hide:file @hide:file
type_record_slot proc(state @State, record, member ast.NodeId) usize { type_record_slot proc(tree @ast.Tree, record, member ast.NodeId) usize {
if (ast.expr_kind(&state.tree, member) != .literal_int) { if (ast.expr_kind(tree, member) != .literal_int) {
for (0..ast.record_field_count(&state.tree, record)) |i| { for (0..ast.record_field_count(tree, record)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, record, i)) field :: ast.field(tree, ast.record_field(tree, record, i))
if (field.name != null and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, member)) if (field.name != null and ast.name_id(tree, field.name?) == ast.name_id(tree, member))
return i return i
} }
return no_index return no_index
} }
ordinal := usize(0) ordinal := usize(0)
for (ast.literal_text(&state.tree, member)) |byte| { for (ast.literal_text(tree, member)) |byte| {
if (byte < '0' or byte > '9') return no_index if (byte < '0' or byte > '9') return no_index
digit :: usize(byte - '0') digit :: usize(byte - '0')
if (ordinal > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index if (ordinal > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index
ordinal = ordinal * 10 + digit ordinal = ordinal * 10 + digit
} }
if (ordinal >= ast.record_field_count(&state.tree, record)) return no_index if (ordinal >= ast.record_field_count(tree, record)) return no_index
if (ast.field(&state.tree, ast.record_field(&state.tree, record, ordinal)).name != null) return no_index if (ast.field(tree, ast.record_field(tree, record, ordinal)).name != null) return no_index
return ordinal return ordinal
} }
@@ -4157,7 +4344,7 @@ type_union_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId
if (field.value) |value| input = value if (field.value) |value| input = value
} }
} }
slot :: if (name != no_node) type_record_slot(state, type, name) else no_index slot :: if (name != no_node) type_record_slot(&state.tree, type, name) else no_index
if (slot == no_index) { if (slot == no_index) {
code diagnostics.Code :: if (name != no_node) .unknown_field else .union_initializer_count code diagnostics.Code :: if (name != no_node) .unknown_field else .union_initializer_count
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
@@ -4309,7 +4496,7 @@ type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error {
} }
} }
} }
if (field.name != null and type_record_slot(state, type, field.name?) != i) { if (field.name != null and type_record_slot(&state.tree, type, field.name?) != i) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span)
return false return false
} }
@@ -4362,7 +4549,7 @@ type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast.
for (0..ast.record_field_count(&state.tree, expression)) |i| { for (0..ast.record_field_count(&state.tree, expression)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, i)) field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, i))
slot := i slot := i
if (field.name) |name| slot = type_record_slot(state, type, name) if (field.name) |name| slot = type_record_slot(&state.tree, type, name)
if (slot >= count or values[slot] != no_node) { if (slot >= count or values[slot] != no_node) {
code :: if (slot < count) diagnostics.Code.duplicate_field code :: if (slot < count) diagnostics.Code.duplicate_field
else if (field.name != null) diagnostics.Code.unknown_field else diagnostics.Code.tuple_arity_mismatch else if (field.name != null) diagnostics.Code.unknown_field else diagnostics.Code.tuple_arity_mismatch
@@ -4376,7 +4563,7 @@ type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast.
for (0..count) |i| { for (0..count) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i))
if (field.name) |name| { if (field.name) |name| {
if (type_record_slot(state, type, name) != i) { if (type_record_slot(&state.tree, type, name) != i) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span)
return no_node return no_node
} }
@@ -4750,7 +4937,7 @@ type_array_update proc(
previous := no_node previous := no_node
if (record) { if (record) {
if (ast.expr_kind(&state.tree, first) == .selection) if (ast.expr_kind(&state.tree, first) == .selection)
index = type_record_slot(state, type, ast.selection_expr(&state.tree, first).member) index = type_record_slot(&state.tree, type, ast.selection_expr(&state.tree, first).member)
if (index == no_index) { if (index == no_index) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unknown_field, ast.node_span(&state.tree, first)) .unknown_field, ast.node_span(&state.tree, first))
@@ -4860,7 +5047,7 @@ type_variant proc(state @mut State, annotation, name ast.NodeId) bool ! Error {
members :: ast.binary_expr(&state.tree, type) members :: ast.binary_expr(&state.tree, type)
return try type_variant(state, members.left, name) or try type_variant(state, members.right, name) return try type_variant(state, members.left, name) or try type_variant(state, members.right, name)
} }
if (kind == .union_type) return type_record_slot(state, type, name) != no_index if (kind == .union_type) return type_record_slot(&state.tree, type, name) != no_index
if (kind != .enum_type) return false if (kind != .enum_type) return false
for (0..ast.enum_member_count(&state.tree, type)) |i| { for (0..ast.enum_member_count(&state.tree, type)) |i| {
member :: ast.field(&state.tree, ast.enum_member(&state.tree, type, i)).name? member :: ast.field(&state.tree, ast.enum_member(&state.tree, type, i)).name?
@@ -5294,9 +5481,9 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution,
if (execution.returned != no_node) { if (execution.returned != no_node) {
type :: try canonical_type(state, execution.result_type) type :: try canonical_type(state, execution.result_type)
if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type) { if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type) {
payload :: ast.typed_constant(&state.tree, execution.returned).value failed :: try type_fallible_failed(state, execution.returned, type, value)
execution.failed = !try same_type(state, ast.typed_constant(&state.tree, payload).type, if (failed == null) return .invalid
ast.binary_expr(&state.tree, type).left) execution.failed = failed?
} }
} }
if (execution.returned != no_node) return .returned if (execution.returned != no_node) return .returned
@@ -6425,7 +6612,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
} }
} }
if (base_kind != .record_type and base_kind != .union_type) return no_node if (base_kind != .record_type and base_kind != .union_type) return no_node
slot :: type_record_slot(state, base, view.member) slot :: type_record_slot(&state.tree, base, view.member)
if (slot == no_index) return no_node if (slot == no_index) return no_node
return try canonical_type(state, ast.field(&state.tree, ast.record_field(&state.tree, base, slot)).type?) return try canonical_type(state, ast.field(&state.tree, ast.record_field(&state.tree, base, slot)).type?)
} }
+175 -22
View File
@@ -11,6 +11,161 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths. # Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = { CASES = {
"compiletime_only_main_cannot_select_another_runtime_function": (
"""
fallback proc() i16 { return -99 }
main proc() i16 {
types := [i16, bool]
return 42
}
""", b"", b"main must be a runtime function with no parameters",
),
"inferred_metadata_locals_preserve_owned_copies_and_updates": (
"""
Config :: struct { children [2]mut ?type, count usize }
compute proc(count usize) i16 {
config := Config { children = [i8, null], count = 0 }
saved :: config
while (config.count < count) {
config.children[0] = i16
config.children[1] = config.children[0]
config.count += 1
}
types := [i8, bool]
before :: types
types = [i16, bool]
tuple := { types[0], config.count }
tuple.0 = i8
if (saved.children[0]? != i8 or saved.count != 0 or before[0] != i8) return -1
if (tuple.0 != i8 or tuple.1 != 2) return -2
for config.children |child| { if child |T| { if (T != i16) return -3 } }
Element :: types[0]
values [count]Element :: [19, 23]
return values[0] + values[1]
}
main proc() i16 { return compute(2) }
""", b"42\n", None,
),
"inferred_type_locals_recurse_and_preserve_cleanup": (
"""
Error :: enum { failed }
count proc(depth usize) i16 {
T := i8
T = i16
defer if (T != i16) unreachable
if (depth == 0) return 21
return count(depth - 1) + 21
}
fail proc() i16 ! Error {
T := ?i16
T = ?i8
errdefer if (T != ?i8) unreachable
return .failed
}
main proc() i16 { return fail() catch { yield count(1) } }
""", b"42\n", None,
),
"inferred_metadata_call_results_and_checked_projections": (
"""
Config :: struct { child type, count usize }
Schema :: alias Config
create proc() Schema { return { child = i8, count = 2 } }
children proc() [2]type { return [i8, bool] }
optional proc() ?type { return i8 }
pair proc() struct { type, usize } { return {i8, 2} }
field_type proc(value i16) i16 {
T := create().child
T = i16
result T :: value
return result
}
indexed_type proc(value i16) i16 {
T := children()[0]
T = i16
result T :: value
return result
}
unwrapped_type proc(value i16) i16 {
T := optional()?
T = i16
result T :: value
return result
}
tuple_type proc(value i16) i16 {
T := pair().0
T = i16
result T :: value
return result
}
compute proc() i16 {
config := create()
saved :: config
config.child = i16
if (saved.child != i8) return -1
return field_type(19) + indexed_type(19) + unwrapped_type(2) + tuple_type(2)
}
main proc() i16 { return compute() }
""", b"42\n", None,
),
"inferred_metadata_value_blocks_matches_and_fallible_calls": (
"""
Error :: enum { failed }
Config :: struct { child type, count usize }
create proc() Config ! Error { return { child = i8, count = 2 } }
compute proc(flag bool) i16 ! Error {
errdefer unreachable
config := try create()
saved :: config
config.child = i16
fields := { yield [i8, bool] }
selected := if (flag) { yield { child = i16, count = config.count } }
else { yield { child = i8, count = usize(1) } }
if (saved.child != i8 or fields[1] != bool or selected.child != i16) return -1
Element :: selected.child
values [selected.count]Element :: [19, 23]
return values[0] + values[1]
}
main proc() i16 { return compute(true) catch -2 }
""", b"42\n", None,
),
"inferred_metadata_locals_require_constant_inputs": (
"""
Config :: struct { child type, count usize }
count proc(value usize) usize {
config := Config { child = i16, count = value }
return config.count
}
runtime proc(value usize) usize { return count(value) }
main proc() usize { return runtime(42) }
""", b"", b"generic argument must be a compile-time value",
),
"inferred_metadata_arrays_keep_readonly_elements": (
"""
compute proc() usize {
types := [i8, bool]
types[0] = i16
return types.len
}
main proc() usize { return compute() }
""", b"", b"indexed assignment requires a writable array element",
),
"immutable_type_aliases_and_scalar_metadata_projections_stay_runtime": (
"""
Config :: struct { child type, count usize }
create proc() Config { return { child = i16, count = 2 } }
Child proc() type { return i16 }
add proc(value i16) i16 {
Local :: alias i16
Other :: alias Child()
amount := create().count
argument Local :: value
result Other :: argument + i16(amount)
return result
}
forward proc(value i16) i16 { return add(value) }
main proc() i16 { return forward(40) }
""", b"42\n", None,
),
"explicit_metadata_locals_compute_without_metadata_inputs": ( "explicit_metadata_locals_compute_without_metadata_inputs": (
""" """
Config :: struct { children [2]mut ?type, count usize } Config :: struct { children [2]mut ?type, count usize }
@@ -3919,15 +4074,6 @@ CASES = {
main proc() Selected() { return 42 } main proc() Selected() { return 42 }
""", b"", b"compile-time execution exceeds its recursion or step limit", """, b"", b"compile-time execution exceeds its recursion or step limit",
), ),
"type_record_procedure_results_do_not_enable_runtime_storage": (
"""
config proc() struct { child type, count i16 } { return { child = i16, count = 42 } }
main proc() i16 {
input :: config()
return input.count
}
""", b"", b"",
),
"type_records_reject_unknown_initializer_field": ( "type_records_reject_unknown_initializer_field": (
""" """
Bad proc() type { Bad proc() type {
@@ -4020,14 +4166,6 @@ CASES = {
main proc() Bad() { return 42 } main proc() Bad() { return 42 }
""", b"", b"outside its runtime type", """, b"", b"outside its runtime type",
), ),
"type_records_do_not_enable_runtime_storage": (
"""
main proc() i16 {
config :: { child = i16, count = 42 }
return config.count
}
""", b"", b"type name cannot be used as a value",
),
"field_reflection_runtime_places_callbacks_and_single_evaluation": ( "field_reflection_runtime_places_callbacks_and_single_evaluation": (
""" """
Point :: struct { value i16, callback @proc(value i16) i16 } Point :: struct { value i16, callback @proc(value i16) i16 }
@@ -11964,13 +12102,28 @@ def check_typeinfo_packages(temporary, executable):
""", """,
b"indexed assignment requires a writable array element", b"indexed assignment requires a writable array element",
), ),
(
"runtime_metadata_escape",
"main proc() i16 { info :: typeinfo!(i16)\nreturn 42 }",
b"record fields require a concrete value type",
),
): ):
cases[name] = ('import "@std/meta"\n' + text, b"", error) cases[name] = ('import "@std/meta"\n' + text, b"", error)
cases["inferred_reflection_locals_compute_without_metadata_parameters"] = (
"""
import "@std/meta"
Record :: struct { child i16, enabled bool }
compute proc(bias i16) i16 {
info :: typeinfo!(Record)
match info {
.record |record|: {
saved :: record.fields[0]
field := saved
field.type = i8
if (saved.type != i16 or field.type != i8) return -1
return bias + i16(record.fields.len)
}
else: return -2,
}
}
main proc() i16 { return compute(40) }
""", b"42\n", None,
)
cases["missing_meta_import"] = ( cases["missing_meta_import"] = (
"main proc() i16 { typeinfo!(i16)\nreturn 42 }", "main proc() i16 { typeinfo!(i16)\nreturn 42 }",
b"", b"typeinfo! requires importing @std/meta", b"", b"typeinfo! requires importing @std/meta",