feat: add records and tuples across execution paths
This commit is contained in:
@@ -22,7 +22,7 @@ run a source file with the interpreter:
|
||||
|
||||
bare `import "../library"` and aliased `library :: import "@library"` declarations load dependencies. relative paths start at the importing package directory; `@` starts at the nearest ancestor containing `build.hon` or `build.bro`, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports.
|
||||
|
||||
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. imported namespaces are not re-exported; missing members and non-namespace selections are diagnosed. general record fields, imported types and visibility attributes are not implemented; unsupported attributes produce syntax diagnostics.
|
||||
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. qualified record types and constructors such as `library.Point` and `library.Point { x = 1 }` use the same declaration identity. imported namespaces are not re-exported; missing members are diagnosed. visibility attributes are not implemented; unsupported attributes produce syntax diagnostics.
|
||||
|
||||
```hon
|
||||
add proc(left, right int) int {
|
||||
@@ -55,7 +55,7 @@ clang -target arm64-apple-macosx11.0.0 -O2 build/factorial.ll -o build/factorial
|
||||
|
||||
`emit-llvm` writes LLVM text to stdout and source diagnostics to stderr. both emission commands return `1` for recoverable source diagnostics while still producing their output when emission and linking succeed. the resulting executable preserves demand-driven traps, reports all source diagnostics, and returns `1` even if unaffected code completes. runtime-generated diagnostics retain source locations; the original source file and compiler are not needed to run the executable. invocation or Clang failures return `2`.
|
||||
|
||||
the backend (`@source/llvm`) consumes the existing typed IR, emits native functions and control flow, and uses entry-block stack slots for locals and instruction results, including non-SSA result IDs. LLVM can promote eligible slots during optimization. arrays use typed native aggregates with by-value copies; loop storage is allocated once per invocation. signed and unsigned arithmetic use checked overflow intrinsics, array accesses retain bounds checks, local initialization flags reset on scope re-entry, globals initialize lazily with cycle checks, and floating operations preserve IEEE behavior without fast-math flags. native calls use the native stack rather than the interpreter's heap-backed frames.
|
||||
the backend (`@source/llvm`) consumes the existing typed IR, emits native functions and control flow, and uses entry-block stack slots for locals and instruction results, including non-SSA result IDs. LLVM can promote eligible slots during optimization. arrays and records use typed native aggregates with by-value copies; loop storage is allocated once per invocation. signed and unsigned arithmetic use checked overflow intrinsics, array accesses retain bounds checks, local initialization flags reset on scope re-entry, globals initialize lazily with cycle checks, and floating operations preserve IEEE behavior without fast-math flags. native calls use the native stack rather than the interpreter's heap-backed frames.
|
||||
|
||||
run the bootstrap suite with `bro test`. after `bro build`, run `python3 tests/native.py` for executable-level interpreter comparisons at both `-O0` and `-O2`, including traps, source diagnostics, array ownership, calling conventions, and CLI error handling.
|
||||
|
||||
@@ -93,6 +93,10 @@ array lengths currently support nonnegative integer literals, unary signs, `+`,
|
||||
|
||||
indices require `usize`; integer literals and flexible inferred values receive this context, while fixed signed integers do not convert implicitly. out-of-bounds access traps before loading or storing, including for empty arrays. reads evaluate the base before the index; indexed assignment evaluates its right-hand side before the destination, each once. leading nested assignment targets follow Bro's declaration ambiguity: write `(matrix[1])[0] = 9`, not `matrix[1][0] = 9`. [programs/array_sum.hon](programs/array_sum.hon) uses a `usize` range bound to index, mutate and sum an array; `./build/honey run programs/array_sum.hon` prints `17`.
|
||||
|
||||
records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default.
|
||||
|
||||
tuple types use `struct { i64, bool }`; tuple values use `{42, true}`, `{42,}` and `{}`. anonymous keyed records, such as `{ x = 40, ready = true }`, infer structural types. fields use `.x` or canonical tuple ordinals such as `.0`, including chained selections. mutable local roots permit field updates; constants, parameters and globals remain readonly, and traversed arrays still require mutable elements. construction, binding, assignment, arguments and returns copy nested records/tuples and arrays by value. fields can be read from returned temporaries; records containing no scalar leaves retain their logical shapes. CLI results print declaration-order values as `{value, value}`.
|
||||
|
||||
`while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry.
|
||||
|
||||
`for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' exact integer type in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum of each integer type.
|
||||
@@ -113,7 +117,7 @@ resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put`
|
||||
|
||||
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. `sema.resolve_packages` additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
|
||||
|
||||
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types and interned array descriptors; query with `check.type_of` and `check.array_type`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types and fixed arrays; type names are not ordinary values. `void` is valid as a function result, not as a parameter or stored value.
|
||||
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, array/record descriptors and construction mappings; query with `check.type_of`, `check.array_type`, `check.record_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types, fixed arrays and local/imported record types; type names are not ordinary runtime values. `void` is valid as a function result, not as a parameter or stored value.
|
||||
|
||||
function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage.
|
||||
|
||||
@@ -131,10 +135,10 @@ type errors carry source spans and diagnostic identities; invalid operands propa
|
||||
|
||||
`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. short-circuit expressions use branches and synthetic boolean local slots to merge results; call arguments are packed in the block reached after evaluating them. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter.
|
||||
|
||||
array IR retains recursive element types, counts and mutability. typed aggregate construction, element addresses and load/store instructions use flattened storage reserved per invocation; nested copies and indexed accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers.
|
||||
aggregate IR retains recursive array element types/counts/mutability and declaration-order record fields with flattened offsets. typed construction, field/element addresses and load/store instructions use storage reserved per invocation; nested copies and accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers.
|
||||
|
||||
IR construction owns derived layout metadata. `ir.add_array` checks shape capacity and computes flattened leaf counts; `ir.add_parameter` and `ir.add_local` create slots with their disjoint array backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, array-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated array results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required.
|
||||
IR construction owns derived layout metadata. `ir.add_array` and `ir.add_record` check shape capacity and compute flattened leaf counts; record shapes own copied field metadata. both aggregate kinds use `ArrayId` shapes and the existing backing segments. `ir.add_parameter` and `ir.add_local` create slots with disjoint aggregate backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, aggregate-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated aggregate results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required.
|
||||
|
||||
`interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned arrays own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. array descriptors also borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
|
||||
`interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned aggregates own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. aggregate descriptors also borrow the module; `ir.aggregate_item` projects a borrowed field/element while preserving zero-cell child shapes. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
|
||||
|
||||
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. compound assignment remains unimplemented.
|
||||
|
||||
Reference in New Issue
Block a user