feat: execute mutable type-valued compile-time procedures
This commit is contained in:
@@ -96,7 +96,9 @@ main proc() i16 {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures accept ordinary parameters as well as `$` parameters: every argument must be supplied explicitly and evaluate to a constant, and every input participates in specialization caching. for example, `Fixed proc(T type, count usize) type { return struct { values [count]T } }` accepts `Fixed(i16, 2)` and reuses its nominal type for equivalent inputs. use `$` when a parameter must be visible in subsequent signature annotations. type-producing bodies support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records; mutable type-valued locals and loops in these bodies remain unsupported. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances.
|
the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures accept ordinary parameters as well as `$` parameters: every argument must be supplied explicitly and evaluate to a constant, and every input participates in specialization caching. for example, `Fixed proc(T type, count usize) type { return struct { values [count]T } }` accepts `Fixed(i16, 2)` and reuses its nominal type for equivalent inputs. use `$` when a parameter must be visible in subsequent signature annotations. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances.
|
||||||
|
|
||||||
|
type-producing bodies support mutable type and value locals, direct binding assignment, scoped blocks, conditional/early returns, integer range and while loops, break/continue and nested/recursive type-producing calls. for example, start with `Result := T`, repeat `Result = [2]Result` in a loop, then `return Result` to generate nested array storage. snapshots retain each local's current value without changing the source AST; scalar operations reuse checking, typed IR and interpreter execution, preserving exact types and overflow diagnostics. type-control execution is limited to 4096 statement/loop-guard steps shared with nested producers; individual scalar evaluations retain the interpreter limits below. only executed bodies are elaborated, while every body is still parsed and name-resolved. field/index assignment, optional captures, cleanup and type-valued aggregate storage in type-producing bodies remain outside this checkpoint.
|
||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ ordinary procedures can compute scalar and nested record/array compile-time valu
|
|||||||
using mutable locals, loops and recursive calls; explicit `$()` expressions use
|
using mutable locals, loops and recursive calls; explicit `$()` expressions use
|
||||||
the same constant evaluation path. compile-time `if` selection and type comparisons
|
the same constant evaluation path. compile-time `if` selection and type comparisons
|
||||||
specialize only the selected body. type-producing procedures also accept ordinary
|
specialize only the selected body. type-producing procedures also accept ordinary
|
||||||
constant inputs, cached and erased like explicit compile-time arguments; general
|
constant inputs, cached and erased like explicit compile-time arguments. mutable
|
||||||
type-valued execution, reflection and generic library acceptance remain open.
|
type/value locals, direct assignments, branches and bounded integer-range/while
|
||||||
|
loops now generate types and storage; general type-valued execution, reflection
|
||||||
|
and generic library acceptance remain open.
|
||||||
the driver retains frontend storage and source provenance through
|
the driver retains frontend storage and source provenance through
|
||||||
`@source/compilation`, with compilation-wide AST node identities.
|
`@source/compilation`, with compilation-wide AST node identities.
|
||||||
file commands retain single-file behavior; directory commands compile the
|
file commands retain single-file behavior; directory commands compile the
|
||||||
@@ -65,7 +67,8 @@ test discovery and FFI remain outside these completed slices.
|
|||||||
- [x] compile-time execution checkpoint: ordinary procedure calls with constant inputs and integer/boolean/string results reuse checking, typed IR and bounded interpreter execution. mutable scalar locals, range/while loops, break/continue, nested/generic/recursive calls, exact numeric types, runtime traps and dependency-file provenance survive argument erasure through interpreter/native execution. runtime parameters and mutable global dependencies are rejected; recompilation retains no prior execution state.
|
- [x] compile-time execution checkpoint: ordinary procedure calls with constant inputs and integer/boolean/string results reuse checking, typed IR and bounded interpreter execution. mutable scalar locals, range/while loops, break/continue, nested/generic/recursive calls, exact numeric types, runtime traps and dependency-file provenance survive argument erasure through interpreter/native execution. runtime parameters and mutable global dependencies are rejected; recompilation retains no prior execution state.
|
||||||
- [x] aggregate/explicit-expression checkpoint: computed nested records/tuples and fixed arrays, contextual/dependent aggregate value parameters, normalized specialization caching and explicit `$()` expressions through interpreter/native execution. preserve by-value isolation, zero-cell shapes, integer widths, string ownership, enum/optional fields and exact IEEE float bits; reject runtime dependencies and values that would retain temporary interpreter storage. repeatable compilation restores erased expressions.
|
- [x] aggregate/explicit-expression checkpoint: computed nested records/tuples and fixed arrays, contextual/dependent aggregate value parameters, normalized specialization caching and explicit `$()` expressions through interpreter/native execution. preserve by-value isolation, zero-cell shapes, integer widths, string ownership, enum/optional fields and exact IEEE float bits; reject runtime dependencies and values that would retain temporary interpreter storage. repeatable compilation restores erased expressions.
|
||||||
- [x] conditional specialization checkpoint: explicit compile-time `if` guards and side-effect-free constant guards in concrete generic procedures select a body before specialization/checking. type equality retains aliases, nominal identity and structural signatures; composed error-set identity/cache keys ignore ordering, grouping and repetition. guard checking, selected-body diagnostics, scoped cleanup, runtime guards and repeatable compilation work through interpreter/native execution; both arms remain parsed and name-resolved.
|
- [x] conditional specialization checkpoint: explicit compile-time `if` guards and side-effect-free constant guards in concrete generic procedures select a body before specialization/checking. type equality retains aliases, nominal identity and structural signatures; composed error-set identity/cache keys ignore ordering, grouping and repetition. guard checking, selected-body diagnostics, scoped cleanup, runtime guards and repeatable compilation work through interpreter/native execution; both arms remain parsed and name-resolved.
|
||||||
- [x] constant-input type-procedure checkpoint: type-producing procedures accept ordinary parameters alongside `$` parameters, require every argument explicitly and evaluate/cache/erase all inputs. generated record/array storage, nominal cache identity, nested calls, dependent contextual aggregate inputs, exact erased-argument checking, source diagnostics, imported producers and repeatable compilation work through interpreter/native execution. mutable type-valued execution remains open.
|
- [x] constant-input type-procedure checkpoint: type-producing procedures accept ordinary parameters alongside `$` parameters, require every argument explicitly and evaluate/cache/erase all inputs. generated record/array storage, nominal cache identity, nested calls, dependent contextual aggregate inputs, exact erased-argument checking, source diagnostics, imported producers and repeatable compilation work through interpreter/native execution.
|
||||||
|
- [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.
|
||||||
- [ ] remaining gate: general type-valued compile-time execution, 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.
|
- [ ] remaining gate: general type-valued compile-time execution, 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.
|
||||||
- [ ] 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`.
|
- [ ] 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.
|
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
|
||||||
|
|||||||
@@ -86,7 +86,15 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
|
|||||||
` while (result < 2) result = result + 1
|
` while (result < 2) result = result + 1
|
||||||
` return result
|
` return result
|
||||||
`}
|
`}
|
||||||
`Fixed proc(N usize) type { return struct { values [N]i16 } }
|
`Fixed proc(N usize) type {
|
||||||
|
` Element := i8
|
||||||
|
` length usize := 0
|
||||||
|
` while (length < N) {
|
||||||
|
` Element = i16
|
||||||
|
` length = length + 1
|
||||||
|
` }
|
||||||
|
` return struct { values [length]Element }
|
||||||
|
`}
|
||||||
`data proc() [2]i16 { return [40, 2] }
|
`data proc() [2]i16 { return [40, 2] }
|
||||||
`main proc() i16 {
|
`main proc() i16 {
|
||||||
` if $(i16 != i16) { return $(i8(128)) }
|
` if $(i16 != i16) { return $(i8(128)) }
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ State :: struct {
|
|||||||
mapping []mut ast.NodeId
|
mapping []mut ast.NodeId
|
||||||
resolution @mut sema.Resolution
|
resolution @mut sema.Resolution
|
||||||
evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
||||||
|
type_execution_depth usize = 0
|
||||||
|
type_execution_remaining usize = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
#! elaborates templates into cached concrete declarations in the caller-owned arena.
|
#! elaborates templates into cached concrete declarations in the caller-owned arena.
|
||||||
@@ -582,7 +584,8 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
|
|||||||
}
|
}
|
||||||
if (is_type(state, view.return_type)) {
|
if (is_type(state, view.return_type)) {
|
||||||
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
||||||
returned :: try return_type_value(state, view.body)
|
constraints_start :: state.rewrites.constraints.items.len
|
||||||
|
returned :: try return_type_value(state, view.body, ast.NodeId(clone_start))
|
||||||
if (returned == no_node) {
|
if (returned == no_node) {
|
||||||
try reject_evaluation(state, call)
|
try reject_evaluation(state, call)
|
||||||
return
|
return
|
||||||
@@ -601,6 +604,8 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
|
|||||||
state.bindings.items[usize(declaration)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = declaration } }
|
state.bindings.items[usize(declaration)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = declaration } }
|
||||||
try arraylist.append(&state.declarations, declaration)
|
try arraylist.append(&state.declarations, declaration)
|
||||||
try expand(state, declaration)
|
try expand(state, declaration)
|
||||||
|
constraint_count :: state.rewrites.constraints.items.len
|
||||||
|
for (constraints_start..constraint_count) |i| try expand(state, state.rewrites.constraints.items[i])
|
||||||
} else {
|
} else {
|
||||||
runtime := arraylist.init(ast.NodeId, state.allocator)
|
runtime := arraylist.init(ast.NodeId, state.allocator)
|
||||||
defer arraylist.deinit(&runtime)
|
defer arraylist.deinit(&runtime)
|
||||||
@@ -947,47 +952,308 @@ evaluate_ir proc(state @mut State, expression, result_type ast.NodeId) Value ! E
|
|||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error {
|
TypeLocal :: struct {
|
||||||
count :: ast.block_statement_count(&state.tree, block)
|
declaration ast.NodeId
|
||||||
for (0..count) |i| {
|
value ast.NodeId
|
||||||
statement :: ast.block_statement(&state.tree, block, i)
|
annotation ast.NodeId
|
||||||
match ast.stmt_kind(&state.tree, statement) {
|
}
|
||||||
.return_stmt: {
|
|
||||||
if (ast.return_stmt(&state.tree, statement).value) |value| return value
|
@hide:file
|
||||||
return no_node
|
TypeExecution :: struct {
|
||||||
|
locals arraylist.ArrayList(TypeLocal)
|
||||||
|
mapping []mut ast.NodeId
|
||||||
|
first_owned ast.NodeId
|
||||||
|
returned ast.NodeId = no_node
|
||||||
|
loop_depth usize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
TypeFlow :: enum { next, returned, break, continue, invalid }
|
||||||
|
|
||||||
|
# Type values cannot enter runtime IR. Execute their control flow here, substituting
|
||||||
|
# owned constants into snapshots; ordinary value operations still use checked IR.
|
||||||
|
@hide:file
|
||||||
|
return_type_value proc(state @mut State, block, first_owned ast.NodeId) ast.NodeId ! Error {
|
||||||
|
# ponytail: snapshots recheck scalar IR; cap type-control work at 4096 steps
|
||||||
|
# until type values can share one persistent checked execution frame.
|
||||||
|
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
|
||||||
|
state.type_execution_depth += 1
|
||||||
|
defer state.type_execution_depth -= 1
|
||||||
|
execution := TypeExecution{
|
||||||
|
locals = arraylist.init(state.allocator),
|
||||||
|
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
|
||||||
|
first_owned = first_owned,
|
||||||
|
}
|
||||||
|
defer arraylist.deinit(&execution.locals)
|
||||||
|
defer mem.free(state.allocator, execution.mapping)
|
||||||
|
mem.set(execution.mapping, no_node)
|
||||||
|
if (try execute_type_block(state, &execution, block) != .returned) return no_node
|
||||||
|
return execution.returned
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
type_step proc(state @mut State, at ast.NodeId) bool ! Error {
|
||||||
|
if (state.type_execution_remaining == 0) {
|
||||||
|
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
|
||||||
|
.comptime_execution_limit, ast.node_span(&state.tree, at))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
state.type_execution_remaining -= 1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) void {
|
||||||
|
if (usize(id) >= execution.mapping.len) return
|
||||||
|
# Substituted arguments belong to the caller, including nominal type identity.
|
||||||
|
if (id < execution.first_owned) {
|
||||||
|
execution.mapping[usize(id)] = id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
execution.mapping[usize(id)] = no_node
|
||||||
|
resolved :: binding(state, id)
|
||||||
|
if (resolved.kind == .declaration) {
|
||||||
|
for (execution.locals.items) |local| {
|
||||||
|
if (local.declaration == resolved.data.declaration) {
|
||||||
|
execution.mapping[usize(id)] = local.value
|
||||||
|
return
|
||||||
}
|
}
|
||||||
.declaration: {
|
|
||||||
declaration :: ast.statement_decl(&state.tree, statement)
|
|
||||||
if (declaration.mutability != .constant) return no_node
|
|
||||||
value :: try evaluate(state, declaration.initializer)
|
|
||||||
if (value.kind == .invalid) return no_node
|
|
||||||
try arraylist.append(&state.rewrites.constraints, statement)
|
|
||||||
}
|
|
||||||
.if_stmt: {
|
|
||||||
conditional :: ast.if_stmt(&state.tree, statement)
|
|
||||||
boolean :: try builtin(state, conditional.condition, .bool)
|
|
||||||
constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, conditional.condition),
|
|
||||||
boolean, conditional.condition)
|
|
||||||
try refresh(state)
|
|
||||||
try arraylist.append(&state.rewrites.constraints, constraint)
|
|
||||||
value :: try evaluate(state, conditional.condition)
|
|
||||||
if (value.kind != .boolean) return no_node
|
|
||||||
branch := no_node
|
|
||||||
if (value.boolean) branch = conditional.then_block
|
|
||||||
else if (conditional.else_block) |other| branch = other
|
|
||||||
if (branch != no_node) {
|
|
||||||
result :: try return_type_value(state, branch)
|
|
||||||
if (result != no_node) return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.block_stmt: {
|
|
||||||
result :: try return_type_value(state, ast.statement_block(&state.tree, statement))
|
|
||||||
if (result != no_node) return result
|
|
||||||
}
|
|
||||||
else: return no_node
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return no_node
|
for (0..ast.child_count(&state.tree, id)) |i|
|
||||||
|
prepare_type_snapshot(state, execution, ast.child(&state.tree, id, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start ast.NodeId) void {
|
||||||
|
if (usize(id) >= execution.mapping.len) return
|
||||||
|
target :: execution.mapping[usize(id)]
|
||||||
|
if (target == no_node or target < start) return
|
||||||
|
resolved := binding(state, id)
|
||||||
|
if (resolved.kind == .declaration) {
|
||||||
|
declaration :: resolved.data.declaration
|
||||||
|
if (usize(declaration) < execution.mapping.len and execution.mapping[usize(declaration)] != no_node)
|
||||||
|
resolved.data.declaration = execution.mapping[usize(declaration)]
|
||||||
|
}
|
||||||
|
state.bindings.items[usize(target)] = resolved
|
||||||
|
for (0..ast.child_count(&state.tree, id)) |i|
|
||||||
|
bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
|
||||||
|
prepare_type_snapshot(state, execution, id)
|
||||||
|
start :: ast.NodeId(ast.node_count(&state.tree))
|
||||||
|
result :: try ast.clone_subtree(state.builder, id, execution.mapping)
|
||||||
|
try refresh(state)
|
||||||
|
bind_type_snapshot(state, execution, id, start)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
nominal_type_value proc(state @State, id ast.NodeId) bool {
|
||||||
|
kind :: ast.expr_kind(&state.tree, id)
|
||||||
|
return kind == .record_type or kind == .union_type or kind == .enum_type or kind == .distinct_type
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
type_execution_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error {
|
||||||
|
# A recursive record needs its cached declaration before expanding its fields.
|
||||||
|
if (is_type(state, annotation) and nominal_type_value(state, expression)) {
|
||||||
|
concrete :: try materialize(state, Value{ kind = .type, node = expression })
|
||||||
|
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), concrete.node, true)
|
||||||
|
try refresh(state)
|
||||||
|
try arraylist.append(&state.rewrites.constraints, constraint)
|
||||||
|
return concrete.node
|
||||||
|
}
|
||||||
|
try expand(state, expression)
|
||||||
|
resolved :: binding(state, expression)
|
||||||
|
if (resolved.kind == .invalid) {
|
||||||
|
state.evaluation_diagnostic = resolved.data.diagnostic
|
||||||
|
return no_node
|
||||||
|
}
|
||||||
|
if (is_type(state, annotation)) {
|
||||||
|
value :: try evaluate(state, expression)
|
||||||
|
if (value.kind != .type) {
|
||||||
|
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
|
||||||
|
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
|
||||||
|
.type_mismatch, ast.node_span(&state.tree, expression))
|
||||||
|
return no_node
|
||||||
|
}
|
||||||
|
concrete :: try materialize(state, value)
|
||||||
|
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), concrete.node, true)
|
||||||
|
try refresh(state)
|
||||||
|
try arraylist.append(&state.rewrites.constraints, constraint)
|
||||||
|
return concrete.node
|
||||||
|
}
|
||||||
|
value :: try evaluate_ir(state, expression, annotation)
|
||||||
|
if (value.kind == .invalid) return no_node
|
||||||
|
concrete :: try materialize(state, value)
|
||||||
|
result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, concrete.node)
|
||||||
|
try refresh(state)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
execute_type_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId) TypeFlow ! Error {
|
||||||
|
saved :: execution.locals.items.len
|
||||||
|
defer execution.locals.items = execution.locals.items[..saved]
|
||||||
|
for (0..ast.block_statement_count(&state.tree, block)) |i| {
|
||||||
|
statement :: ast.block_statement(&state.tree, block, i)
|
||||||
|
if (!try type_step(state, statement)) return .invalid
|
||||||
|
flow :: try execute_type_statement(state, execution, statement)
|
||||||
|
if (flow != .next) return flow
|
||||||
|
}
|
||||||
|
return .next
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
|
||||||
|
match ast.stmt_kind(&state.tree, statement) {
|
||||||
|
.return_stmt: {
|
||||||
|
if (ast.return_stmt(&state.tree, statement).value) |value| {
|
||||||
|
expression :: try type_snapshot(state, execution, value)
|
||||||
|
execution.returned = try type_execution_value(state, expression, try builtin(state, expression, .type))
|
||||||
|
if (execution.returned != no_node) return .returned
|
||||||
|
}
|
||||||
|
return .invalid
|
||||||
|
}
|
||||||
|
.declaration: {
|
||||||
|
declaration :: ast.statement_decl(&state.tree, statement)
|
||||||
|
expression :: try type_snapshot(state, execution, declaration.initializer)
|
||||||
|
annotation := no_node
|
||||||
|
if (declaration.type) |type| annotation = try type_snapshot(state, execution, type)
|
||||||
|
else if (nominal_type_value(state, expression)) annotation = try builtin(state, expression, .type)
|
||||||
|
else {
|
||||||
|
try expand(state, expression)
|
||||||
|
annotation = try expression_type(state, expression, 0)
|
||||||
|
if (annotation == no_node) {
|
||||||
|
value :: try evaluate(state, expression)
|
||||||
|
if (value.kind == .type) annotation = try builtin(state, expression, .type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (annotation == no_node) return .invalid
|
||||||
|
value :: try type_execution_value(state, expression, annotation)
|
||||||
|
if (value == no_node) return .invalid
|
||||||
|
try arraylist.append(&execution.locals, TypeLocal{
|
||||||
|
declaration = statement, value = value, annotation = annotation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.assignment_stmt: {
|
||||||
|
assignment :: ast.assignment_stmt(&state.tree, statement)
|
||||||
|
resolved :: binding(state, assignment.target)
|
||||||
|
index := no_index
|
||||||
|
if (resolved.kind == .declaration) {
|
||||||
|
for (execution.locals.items) |local, i| {
|
||||||
|
if (local.declaration == resolved.data.declaration) index = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index == no_index
|
||||||
|
or ast.node_class(&state.tree, execution.locals.items[index].declaration) != .statement
|
||||||
|
or ast.statement_decl(&state.tree, execution.locals.items[index].declaration).mutability != .variable) {
|
||||||
|
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
|
||||||
|
.invalid_assignment_target, ast.node_span(&state.tree, assignment.target))
|
||||||
|
return .invalid
|
||||||
|
}
|
||||||
|
expression :: try type_snapshot(state, execution, assignment.value)
|
||||||
|
value :: try type_execution_value(state, expression, execution.locals.items[index].annotation)
|
||||||
|
if (value == no_node) return .invalid
|
||||||
|
execution.locals.items[index].value = value
|
||||||
|
}
|
||||||
|
.if_stmt, .while_stmt: {
|
||||||
|
looping :: ast.stmt_kind(&state.tree, statement) == .while_stmt
|
||||||
|
condition := no_node
|
||||||
|
body := no_node
|
||||||
|
other := no_node
|
||||||
|
if (looping) {
|
||||||
|
view :: ast.while_stmt(&state.tree, statement)
|
||||||
|
if (view.capture != null) return .invalid
|
||||||
|
condition = view.condition
|
||||||
|
body = view.body
|
||||||
|
} else {
|
||||||
|
view :: ast.if_stmt(&state.tree, statement)
|
||||||
|
if (view.capture != null) return .invalid
|
||||||
|
condition = view.condition
|
||||||
|
body = view.then_block
|
||||||
|
if (view.else_block) |branch| other = branch
|
||||||
|
}
|
||||||
|
if (looping) execution.loop_depth += 1
|
||||||
|
defer if (looping) execution.loop_depth -= 1
|
||||||
|
invariant :: branch_constant(state, condition, 0)
|
||||||
|
value := Value{}
|
||||||
|
while (true) {
|
||||||
|
if (!try type_step(state, condition)) return .invalid
|
||||||
|
if (!invariant or value.kind == .invalid) {
|
||||||
|
expression :: try type_snapshot(state, execution, condition)
|
||||||
|
boolean :: try builtin(state, expression, .bool)
|
||||||
|
value = try evaluate_ir(state, expression, boolean)
|
||||||
|
}
|
||||||
|
if (value.kind != .boolean) return .invalid
|
||||||
|
if (!value.boolean) {
|
||||||
|
if (!looping and other != no_node) return try execute_type_block(state, execution, other)
|
||||||
|
return .next
|
||||||
|
}
|
||||||
|
flow :: try execute_type_block(state, execution, body)
|
||||||
|
if (!looping or flow == .returned or flow == .invalid) return flow
|
||||||
|
if (flow == .break) return .next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.for_stmt: return try execute_type_for(state, execution, statement)
|
||||||
|
.block_stmt: return try execute_type_block(state, execution, ast.statement_block(&state.tree, statement))
|
||||||
|
.break_stmt, .continue_stmt: {
|
||||||
|
if (execution.loop_depth == 0) {
|
||||||
|
code :: if (ast.stmt_kind(&state.tree, statement) == .break_stmt)
|
||||||
|
diagnostics.Code.break_outside_loop else diagnostics.Code.continue_outside_loop
|
||||||
|
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, statement))
|
||||||
|
return .invalid
|
||||||
|
}
|
||||||
|
return if (ast.stmt_kind(&state.tree, statement) == .break_stmt) TypeFlow.break else TypeFlow.continue
|
||||||
|
}
|
||||||
|
else: return .invalid
|
||||||
|
}
|
||||||
|
return .next
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
execute_type_for proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
|
||||||
|
loop :: ast.for_stmt(&state.tree, statement)
|
||||||
|
if (loop.pointer_capture or loop.index_capture or ast.expr_kind(&state.tree, loop.iterable) != .range) return .invalid
|
||||||
|
bounds :: ast.range_expr(&state.tree, loop.iterable)
|
||||||
|
start :: try type_snapshot(state, execution, bounds.start)
|
||||||
|
end :: try type_snapshot(state, execution, bounds.end)
|
||||||
|
typed_bound :: if (flexible(state, start)) end else start
|
||||||
|
annotation :: try expression_type(state, typed_bound, 0)
|
||||||
|
if (annotation == no_node or !try integer_type(state, annotation)) return .invalid
|
||||||
|
current := try type_execution_value(state, start, annotation)
|
||||||
|
limit :: try type_execution_value(state, end, annotation)
|
||||||
|
if (current == no_node or limit == no_node) return .invalid
|
||||||
|
saved :: execution.locals.items.len
|
||||||
|
defer execution.locals.items = execution.locals.items[..saved]
|
||||||
|
if (loop.capture) |capture| try arraylist.append(&execution.locals, TypeLocal{
|
||||||
|
declaration = capture, value = current, annotation = annotation,
|
||||||
|
})
|
||||||
|
execution.loop_depth += 1
|
||||||
|
defer execution.loop_depth -= 1
|
||||||
|
while (true) {
|
||||||
|
if (!try type_step(state, statement)) return .invalid
|
||||||
|
span :: ast.node_span(&state.tree, statement)
|
||||||
|
operator ast.BinaryOperator :: if (bounds.inclusive) .less_equal else .less
|
||||||
|
comparison :: try ast.add_binary(state.builder, span, operator, current, limit)
|
||||||
|
try refresh(state)
|
||||||
|
keep :: try evaluate(state, comparison)
|
||||||
|
if (keep.kind != .boolean) return .invalid
|
||||||
|
if (!keep.boolean) return .next
|
||||||
|
if (loop.capture != null) execution.locals.items[saved].value = current
|
||||||
|
flow :: try execute_type_block(state, execution, loop.body)
|
||||||
|
if (flow == .returned or flow == .invalid) return flow
|
||||||
|
if (flow == .break) return .next
|
||||||
|
if (try value_equal(state, try evaluate(state, current), try evaluate(state, limit))) return .next
|
||||||
|
one :: try ast.add_integer(state.builder, span, 1)
|
||||||
|
increment :: try ast.add_binary(state.builder, span, .add, current, one)
|
||||||
|
try refresh(state)
|
||||||
|
current = try type_execution_value(state, increment, annotation)
|
||||||
|
if (current == no_node) return .invalid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
@@ -1586,6 +1852,17 @@ infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []m
|
|||||||
materialize proc(state @mut State, value Value) Value ! Error {
|
materialize proc(state @mut State, value Value) Value ! Error {
|
||||||
result := value
|
result := value
|
||||||
span :: ast.node_span(&state.tree, value.node)
|
span :: ast.node_span(&state.tree, value.node)
|
||||||
|
if (value.kind == .type and nominal_type_value(state, value.node)) {
|
||||||
|
# Refer to declared nominal types, rather than reusing their definition as
|
||||||
|
# an annotation and turning its value-role checker cell into an annotation.
|
||||||
|
for (state.declarations.items) |declaration| {
|
||||||
|
if (ast.node_class(&state.tree, declaration) == .statement
|
||||||
|
and ast.statement_decl(&state.tree, declaration).initializer == value.node) {
|
||||||
|
result.node = try reference(state, value.node, declaration)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (value.kind == .integer) {
|
if (value.kind == .integer) {
|
||||||
result.node = try ast.add_integer(state.builder, span, value.magnitude)
|
result.node = try ast.add_integer(state.builder, span, value.magnitude)
|
||||||
if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node)
|
if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node)
|
||||||
|
|||||||
+169
-1
@@ -11,6 +11,168 @@ 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 = {
|
||||||
|
"type_execution_retains_existing_nominal_type_identity": (
|
||||||
|
"""
|
||||||
|
Original :: struct { value i16 }
|
||||||
|
Identity proc(T type) type {
|
||||||
|
Result := T
|
||||||
|
Same :: Result
|
||||||
|
return Same
|
||||||
|
}
|
||||||
|
Box proc(T type) type { return struct { value T } }
|
||||||
|
main proc() i16 {
|
||||||
|
first Original :: { value = 42 }
|
||||||
|
same Identity(Original) :: first
|
||||||
|
boxed Box(Identity(Original)) :: { value = same }
|
||||||
|
cached Box(Original) :: boxed
|
||||||
|
return cached.value.value
|
||||||
|
}
|
||||||
|
""", b"42\n", None,
|
||||||
|
),
|
||||||
|
"type_execution_mutable_types_and_nested_producers": (
|
||||||
|
"""
|
||||||
|
Nest proc(T type, depth usize) type {
|
||||||
|
Result type := T
|
||||||
|
count usize := 0
|
||||||
|
while (count < depth) {
|
||||||
|
Result = [2]Result
|
||||||
|
count = count + 1
|
||||||
|
}
|
||||||
|
return Result
|
||||||
|
}
|
||||||
|
Box proc(T type) type { return struct { value T } }
|
||||||
|
main proc() i16 {
|
||||||
|
value Box(Nest(i16, 2)) :: { value = [[20, 20], [1, 1]] }
|
||||||
|
same Box(Nest(i16, 1 + 1)) :: value
|
||||||
|
return same.value[0][0] + same.value[0][1] + same.value[1][0] + same.value[1][1]
|
||||||
|
}
|
||||||
|
""", b"42\n", None,
|
||||||
|
),
|
||||||
|
"type_execution_range_captures_scope_and_transfers": (
|
||||||
|
"""
|
||||||
|
Storage proc() type {
|
||||||
|
Result := i8
|
||||||
|
size usize := 0
|
||||||
|
for (0..usize(5)) |index| {
|
||||||
|
if (index == 0) continue
|
||||||
|
if (index == 3) break
|
||||||
|
next :: [index]Result
|
||||||
|
Result = next
|
||||||
|
size = size + 1
|
||||||
|
}
|
||||||
|
while (size < 3) {
|
||||||
|
size = size + 1
|
||||||
|
if (size == 3) continue
|
||||||
|
Result = bool
|
||||||
|
}
|
||||||
|
return struct { values Result, bias i16 = i16(size) }
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
value Storage() :: { values = [[19], [20]] }
|
||||||
|
return i16(value.values[0][0]) + i16(value.values[1][0]) + value.bias
|
||||||
|
}
|
||||||
|
""", b"42\n", None,
|
||||||
|
),
|
||||||
|
"type_execution_early_return_and_recursive_calls": (
|
||||||
|
"""
|
||||||
|
Nest proc(T type, depth usize) type {
|
||||||
|
if (depth == 0) return T
|
||||||
|
return [2]Nest(T, depth - 1)
|
||||||
|
}
|
||||||
|
Select proc(wide bool) type {
|
||||||
|
Result := i8
|
||||||
|
if (wide) {
|
||||||
|
Result = Nest(i16, 1)
|
||||||
|
return Result
|
||||||
|
} else { Result = [2]Result }
|
||||||
|
return Result
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
wide Select(true) :: [40, 2]
|
||||||
|
narrow Select(false) :: [1, 1]
|
||||||
|
return wide[0] + wide[1] + i16(narrow[0] - narrow[1])
|
||||||
|
}
|
||||||
|
""", b"42\n", None,
|
||||||
|
),
|
||||||
|
"type_execution_inclusive_narrow_range_does_not_overflow": (
|
||||||
|
"""
|
||||||
|
Storage proc() type {
|
||||||
|
count usize := 0
|
||||||
|
for (u8(254)..=u8(255)) |_| count = count + 1
|
||||||
|
for (i8(2)..i8(-2)) |_| count = 99
|
||||||
|
return [count]i16
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
values Storage() :: [40, 2]
|
||||||
|
return values[0] + values[1]
|
||||||
|
}
|
||||||
|
""", b"42\n", None,
|
||||||
|
),
|
||||||
|
"type_execution_rejects_constant_assignment": (
|
||||||
|
"""
|
||||||
|
Bad proc() type {
|
||||||
|
T :: i16
|
||||||
|
T = bool
|
||||||
|
return T
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
value Bad() :: 42
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
""", b"", b"assignment requires a mutable local binding",
|
||||||
|
),
|
||||||
|
"type_execution_rejects_value_in_type_local": (
|
||||||
|
"""
|
||||||
|
Bad proc() type {
|
||||||
|
T := i16
|
||||||
|
T = 42
|
||||||
|
return T
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
value Bad() :: 42
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
""", b"", b"incompatible value type",
|
||||||
|
),
|
||||||
|
"type_execution_preserves_scalar_type_and_overflow": (
|
||||||
|
"""
|
||||||
|
Bad proc() type {
|
||||||
|
count u8 := 255
|
||||||
|
count = count + 1
|
||||||
|
return [count]i16
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
value Bad() :: 42
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
""", b"", b"integer arithmetic overflow",
|
||||||
|
),
|
||||||
|
"type_execution_checks_guard_operands": (
|
||||||
|
"""
|
||||||
|
Bad proc() type {
|
||||||
|
T := i16
|
||||||
|
while (false and 1) T = bool
|
||||||
|
return T
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
value Bad() :: 42
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
""", b"", b"operator requires compatible operand types",
|
||||||
|
),
|
||||||
|
"type_execution_is_bounded": (
|
||||||
|
"""
|
||||||
|
Bad proc() type {
|
||||||
|
counter usize := 0
|
||||||
|
while (counter >= 0) counter = counter + 1
|
||||||
|
return i16
|
||||||
|
}
|
||||||
|
main proc() i16 {
|
||||||
|
value Bad() :: 42
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
""", b"", b"compile-time execution exceeds its recursion or step limit",
|
||||||
|
),
|
||||||
"type_producer_ordinary_inputs_generate_storage_and_cache_identity": (
|
"type_producer_ordinary_inputs_generate_storage_and_cache_identity": (
|
||||||
"""
|
"""
|
||||||
Fixed proc(T type, count usize) type { return struct { values [count]T } }
|
Fixed proc(T type, count usize) type { return struct { values [count]T } }
|
||||||
@@ -3076,7 +3238,13 @@ def check_specialization_packages(temporary, executable):
|
|||||||
(library / "types.hon").write_text(textwrap.dedent("""
|
(library / "types.hon").write_text(textwrap.dedent("""
|
||||||
Box proc(T type) type { return struct { value T } }
|
Box proc(T type) type { return struct { value T } }
|
||||||
Fixed proc($T type, N usize) type {
|
Fixed proc($T type, N usize) type {
|
||||||
return struct { values [N]mut T }
|
Element := i8
|
||||||
|
length usize := 0
|
||||||
|
while (length < N) {
|
||||||
|
Element = T
|
||||||
|
length = length + 1
|
||||||
|
}
|
||||||
|
return struct { values [length]mut Element }
|
||||||
}
|
}
|
||||||
""").strip() + "\n")
|
""").strip() + "\n")
|
||||||
(library / "functions.hon").write_text(textwrap.dedent("""
|
(library / "functions.hon").write_text(textwrap.dedent("""
|
||||||
|
|||||||
Reference in New Issue
Block a user