Compare commits
83 Commits
0593786553
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a74dd2e960 | |||
| 2a4efd0b7f | |||
| 22b40d3718 | |||
| 1f457d6b41 | |||
| e036070bf2 | |||
| 4fea18b834 | |||
| ee7eeac894 | |||
| ebae8e712c | |||
| 4a1b30d9d5 | |||
| 15cbebe40f | |||
| 0625359d62 | |||
| 83b6153e72 | |||
| 32a7a1de24 | |||
| cc39c96fc0 | |||
| c8890b4355 | |||
| 8e790b30cc | |||
| 6748b38eac | |||
| c65f41db43 | |||
| b1cc97cc77 | |||
| e90d4f0829 | |||
| 31a1bf1995 | |||
| 30a98db379 | |||
| d7e637dc3b | |||
| 670ba8f756 | |||
| 9838b253ee | |||
| 46fc7f4c46 | |||
| b832504f10 | |||
| 9ccb9be01f | |||
| 73684f856a | |||
| 026fe36325 | |||
| 6f892bbfcc | |||
| c6acff91fd | |||
| 5cf9e15661 | |||
| dc7b2e40e2 | |||
| 20539ab455 | |||
| 741ebbf484 | |||
| 440ef93a23 | |||
| ae5314462a | |||
| 443de49d55 | |||
| ca53dab86b | |||
| e00a2abe27 | |||
| 88b51274df | |||
| 80867825e3 | |||
| 0aca64a77e | |||
| 0f5d3c222a | |||
| 08b171d3ff | |||
| c3e10b3bed | |||
| 8d1a98f982 | |||
| f062389ccc | |||
| d5a351606d | |||
| 84e3ed0cc8 | |||
| 4a134b49dd | |||
| 182bb3c3e0 | |||
| bf65f3c2c3 | |||
| e5215c4092 | |||
| 85c60f2416 | |||
| 8c174e8756 | |||
| 303a21a8f5 | |||
| 565733666e | |||
| 4de7c8bfc3 | |||
| ecd20675ea | |||
| 6886d743b5 | |||
| 9058135326 | |||
| cd64afbb40 | |||
| 7856f4f963 | |||
| 676f3666b4 | |||
| 9966a1f523 | |||
| 280059d83d | |||
| 3e93c6f000 | |||
| 0385e46295 | |||
| c40d09b5cc | |||
| ac7ab63b9e | |||
| d834ff9587 | |||
| abd85e531b | |||
| 69eb3ecba2 | |||
| a6d6a5b0a8 | |||
| e54a51327c | |||
| 45359c0cdf | |||
| c01d935a7b | |||
| 2c11772931 | |||
| 2761c77182 | |||
| 678bb2583b | |||
| 675fa5049f |
@@ -10,12 +10,20 @@ build Honey from this directory:
|
||||
bro build
|
||||
```
|
||||
|
||||
the bootstrap must support contextual `&[]` literals for mutable slices and prefixed integer literals (`0b`, `0o`, `0x`); collection initialization uses contextual empty slices and AST flag masks use binary spelling. if the installed `bro` predates either feature, rebuild Bro with `make -C ../brolang build` and use `../brolang/build/brolang build`. zero-sized elements with nonzero logical lengths still use the memory module's specialized storage handling.
|
||||
|
||||
run a source file with the interpreter:
|
||||
|
||||
```sh
|
||||
./build/honey run example.hon
|
||||
```
|
||||
|
||||
`run`, `emit-llvm` and `build` also accept a directory. directory inputs compile its `.hon` and `.bro` files in bytewise filename order, excluding `.test.hon`/`.test.bro` and non-file entries; a file input still compiles only that file, not its siblings. declarations share a scope within each package, while dependencies have separate scopes and cannot supply the root package's `main`.
|
||||
|
||||
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. 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. declarations are public by default; bare `@hide` and `@hide:package` permit sibling access but hide declarations from importing packages. `@hide:file` limits a declaration to its originating file, allowing independent private names in sibling files. unknown visibility attributes produce syntax diagnostics.
|
||||
|
||||
```hon
|
||||
add proc(left, right int) int {
|
||||
return left + right
|
||||
@@ -27,12 +35,42 @@ main proc() int {
|
||||
}
|
||||
```
|
||||
|
||||
this prints `42`. interpreted `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:
|
||||
|
||||
```sh
|
||||
./build/honey build programs/factorial.hon -o build/factorial -O2
|
||||
./build/factorial
|
||||
```
|
||||
|
||||
this prints `120`. `build` requires `-o <executable>` and accepts `-O0` (the default) or `-O2` as its final option. Honey emits textual LLVM IR and invokes `clang` from `PATH` without a shell, targeting `arm64-apple-macosx11.0.0`. Apple Clang 21 is the verified toolchain; LLVM libraries, LLVM bindings and a C emission stage are not required. the temporary IR file is private and removed after compilation, including on failure.
|
||||
|
||||
inspect or compile the LLVM text separately:
|
||||
|
||||
```sh
|
||||
./build/honey emit-llvm programs/factorial.hon > build/factorial.ll
|
||||
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 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.
|
||||
|
||||
the scalar-function package acceptance gate is verified by the native suite: root and dependency sibling files, transitive/shared imports, same-spelled declarations with distinct signatures, file-local aliases and conflicts, missing packages/members, argument errors and dependency-file diagnostics. file-command checks cover `run`, `emit-llvm` and `build`, loading dependency siblings while excluding root siblings.
|
||||
|
||||
dependency provenance checks cover lexer, parser, resolver, checker and lowering diagnostics, plus overflow, bounds, local initialization and global-cycle traps through transitive calls. they assert originating filenames and line/byte-column positions, compare reached and untaken paths, and run generated executables with the source tree temporarily removed.
|
||||
|
||||
the native bootstrap accepts either a no-argument `main` or `main proc(init process.Init)`. it supplies `init.io` and `init.args` from native startup `argc`/`argv`. startup scans each argument once into a heap-allocated array of borrowed zero-terminated slices (`process.Args.values: [][;0]u8`), without copying strings. the descriptor array and borrowed strings remain valid until `main` returns; startup then frees the descriptors, including on a fallible return. allocation failure exits with status `2`. `init.args.values.len` includes the executable name, and `init.args.values[index]` directly accesses an argument after a length guard. callers can supply their own `process.Args { values = slices[..] }` for testing.
|
||||
|
||||
`process.run(allocator, arguments)` starts a child synchronously using Darwin `posix_spawnp`, searches `PATH`, and inherits the environment and standard streams. argument strings are borrowed zero-terminated slices; no shell parsing occurs. it releases temporary argument descriptors on every return, retries interrupted waits, and returns the exit code or `128 + signal` for signal termination.
|
||||
|
||||
source loading uses the supplied `io.Io` for opening, reading, and closing files. `io.open` accepts a borrowed zero-terminated slice (`[;0]u8`), including `init.args.values[index]` without copying its storage. the bootstrap supports checked sentinel-slice construction with `pointer[..length;0]`. `io.read_all(allocator, reader, maximum)` reads to EOF into a caller-owned `ArrayList(u8)`; release it with `arraylist.deinit`. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps `limit_exceeded` to `source_too_large` at its `u32` source-size limit.
|
||||
|
||||
`@std/fs` contains the Darwin filesystem operations used by source loading: `canonical` returns an owned symlink-resolved path (free with the same allocator), while `kind` and `exists` classify paths without exposing C mode bits. `open_directory` returns an owned handle released with `close_directory`; `next` returns a borrowed entry whose name expires on the next call or close. enumeration order is unspecified. `read_file(allocator, runtime, path, maximum)` reads a regular file through the supplied `io.Io`, closes it on success/error, and returns caller-owned bytes released with `arraylist.deinit`; exceeding the bound returns `limit_exceeded`. metadata and directory discovery use the host filesystem. the compiler's `@source/loader` retains project-root/import rules, production-file filtering, deterministic sorting and the `u32` source-size limit, with no direct libc calls.
|
||||
|
||||
a recursive example is in [programs/factorial.hon](programs/factorial.hon); `./build/honey run programs/factorial.hon` prints `120`.
|
||||
|
||||
an iterative example is in [programs/iterative_factorial.hon](programs/iterative_factorial.hon); `./build/honey run programs/iterative_factorial.hon` also prints `120`.
|
||||
@@ -41,6 +79,120 @@ the parser supports arithmetic expressions with nested parenthesized groups, suc
|
||||
|
||||
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow newlines and a trailing comma; outside parentheses, newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
|
||||
|
||||
generic procedures mark compile-time parameters with `$`, such as `identity proc($T type, value T) T`. pass all parameters explicitly (`identity(i16, 42)`), omit every compile-time argument when runtime argument types or an expected result determine them (`identity(value)`), or supply a declaration-order prefix of compile-time arguments and infer the rest. for example, `io.print("{}", writer, {value})` supplies the format while inferring the tuple type. inference follows pointers, arrays, slices, optionals, fallible channels, callback signatures and specialized record constructors, including array counts; conflicting or undetermined arguments are diagnosed. specializations are cached by declaration and compile-time arguments, retain nominal generated-record identity, and erase compile-time parameters from runtime calls.
|
||||
|
||||
expected result types flow from typed bindings, assignments, returns, ordinary call parameters, aggregate fields/elements, defaults, match arms, value-block yields and cleanup handlers. they also reach explicit `$()` expressions, `some!` payloads and arithmetic/bitwise operands; shift counts keep their independent types. rigid runtime arguments constrain inference before result context and contextual literal defaults; result context also types nested generic calls. `answer proc($T type) T { return 42 }` can therefore be called as `value i16 :: answer()`. optional presence and nominal generated types remain exact, explicit arguments are never overridden, and type-producing procedures still require every input explicitly.
|
||||
|
||||
```hon
|
||||
Fixed proc($T type, $N usize) type {
|
||||
return struct { values [N]mut T }
|
||||
}
|
||||
|
||||
first proc($T type, $N usize, value @Fixed(T, N)) T {
|
||||
return value.values[0]
|
||||
}
|
||||
|
||||
main proc() i16 {
|
||||
fixed Fixed(i16, 2) :: { values = [42, 0] }
|
||||
return first(&fixed)
|
||||
}
|
||||
```
|
||||
|
||||
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 and aggregate construction/validation 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. cleanup remains outside this checkpoint.
|
||||
|
||||
field/index assignments also update initialized, materializable aggregate locals in type-producing bodies, including nested record fields and mutable fixed-array elements. for example, `counts [2]mut usize := [0, 0]` followed by `counts[1] = 2` can generate `[counts[1]]i16` storage. array snapshots preserve earlier copies; scalar and ordinary record-field operations reuse checked IR. later loop iterations, generated lengths and defaults observe updates. ordinary mutability, index-type/bounds, field and exact value-type checks still apply, including immutable payload captures and narrow arithmetic overflow. imported producers and repeated compilation retain source diagnostics and nominal cache identity; pointer/slice-backed mutation remains open.
|
||||
|
||||
type-producing bodies also iterate materializable fixed arrays and strings with immutable by-value element captures and optional `usize` indices: `for counts |count, index| { length = length + count + index }` can compute generated storage. nested loops, aggregate captures, empty collections, break/continue and early returns share the type-control execution bound. array places retain their entry-time indices and observe later element updates; temporary arrays and strings are evaluated once. ordinary constant-input procedures additionally support slice and pointer captures through checked IR, provided only materializable results escape. type-producing loop bodies still reject storage-bearing captures rather than retaining temporary interpreter addresses.
|
||||
|
||||
`inline for` expands constant integer ranges, fixed arrays (including arrays of `type`), heterogeneous tuples and strings into separately checked runtime bodies. captures are immutable exact-type constants; collection indices are `usize`. for example, `inline for [i16, bool] |T| { if (T == i16) { value T :: 42 } else { value T :: true } }` checks only each iteration's selected branch. bounds/iterables are computed before expansion, empty loops skip body elaboration, and nested expansions share the 4096-step type-control bound. `break` and `continue` targeting the inline loop must be selected entirely at compile time; selected exits prune the remaining body and `break` skips subsequent iterations. nested ordinary loops retain runtime transfers, while runtime returns, errors and scoped cleanup retain ordinary semantics. runtime-dependent inputs, pointer captures and storage-bearing values that cannot escape constant evaluation are diagnosed. metadata collections and inline-match syntax remain open.
|
||||
|
||||
inline tuples may mix types and values, including nested aggregates, strings, optionals and tagged unions. parenthesize tuple literals in loop headers, such as `inline for ({i16, usize(2), "ok"}) |item, index| { ... }`; use a compile-time index condition to select operations for each field's exact type. constant helper results, tuple projections and erased generic tuple parameters use the same owned snapshots. the entire tuple must be materializable before iteration, even with discarded captures or an early `break`. type-producing bodies borrow local tuple places, observing later field updates without changing earlier copies or captures. named records are not tuple collections, and ordinary `for` still requires homogeneous sequence storage.
|
||||
|
||||
initialized fixed arrays of `type` live entirely inside compile-time execution, including nested arrays, explicit/inferred annotations, `.len`, checked `usize` indexing and collection captures. for example, `types [2]mut type := [i8, bool]`, then `types[1] = i16` and `return [types.len]types[1]`, generates `[2]i16` without runtime type metadata. whole-array and nested indexed assignments preserve shape and mutability at every level; copies and loop captures retain earlier values while borrowed array places observe later updates.
|
||||
|
||||
initialized records/tuples containing `type` fields live inside compile-time producers. local schemas, typed/contextual/anonymous construction, defaults, nested records/arrays, field/index reads and updates, whole-value replacement and by-value collection captures retain owned snapshots. for example, `config struct { child type, count usize } := { child = i8, count = 2 }`, then `config.child = i16` and `return [config.count]config.child`, generates `[2]i16`. earlier copies and loop captures remain independent, while borrowed array places observe later updates. type leaves retain nominal identity; ordinary value leaves reuse checked constant execution. unknown/duplicate/missing fields, invalid types/defaults, readonly places, narrow overflow and dependency provenance retain diagnostics. record validation/construction shares the type-control bound. runtime type-valued records remain unsupported.
|
||||
|
||||
type-valued records/tuples may also start with `undefined`: `config struct { child type, count usize } := undefined`, then `config.child = i16` and `config.count = 2`. nested record fields, arrays of records, tuple ordinals and `field!` updates use the same persistent snapshots as initialized storage; partial copies retain independent initialization state. defaults do not initialize `undefined` fields. initialized leaves and array lengths can be read while other leaves remain uninitialized, but premature leaf reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results cross compile-time procedure boundaries and supply erased generic inputs, generated storage and defaults. empty shapes are initialized vacuously; each expanded record field/array cell shares the construction bound.
|
||||
|
||||
optionals containing types also stay in owned compile-time storage: `?type`, nested `??type`, and optional records/arrays of metadata support null, contextual payloads, explicit `some!`, whole-value updates, immutable captures, null comparisons and checked unwraps. `some!(null)` preserves an absent inner optional inside a present outer one. constant-input procedures exchange these snapshots; erased generic inputs retain content-based specialization identity and nominal type leaves. `if`/`while` captures and conditional expressions select generated types or runtime bodies without runtime metadata. for generated array children, bind the unwrap first (`T :: selected?`, then `[count]T`); postfix `?` after an array type unwraps the whole expression. undefined presence and incomplete payloads diagnose before observation or procedure boundaries. runtime metadata storage remains unsupported.
|
||||
|
||||
fallible channels containing types also execute entirely at compile time: `type ! Error`, metadata records/arrays in either channel, and nested optionals retain owned active payloads. every producer input is explicit and constant; erased generic inputs, whole-value replacement and saved copies preserve exact nominal leaves and content-based specialization identity. `try` checks the complete error subset before propagating; `catch` captures an immutable error snapshot and selects generated types or runtime handlers. named metadata structs and tagged unions compose with ordinary error enums without runtime reflection. returns snapshot before scoped LIFO `defer`/`errdefer`; fallible `void` also permits bare returns and successful fallthrough. incomplete storage, invalid channels/payloads, capture mutation and runtime inputs diagnose. imported private types survive source/session disposal and repeated compilation. runtime locals still cannot store metadata aggregates; keep those values inside compile-time producers or pass them directly to erased inputs.
|
||||
|
||||
tagged unions containing types use the same owned compile-time storage. `Choice :: union(enum) { child type, empty void }` supports typed/contextual construction, active payload reads and nested field/index updates, immutable match captures and whole-value variant replacement. direct payload writes require the selected variant to be active; they never switch its tag. saved copies, constant-input procedure arguments/results and erased generic inputs preserve payloads and nominal type identity while generating runtime storage/defaults. `undefined` unions have no active variant until a whole-value assignment initializes them, including inside records and arrays. explicit enum tags must contain every variant, but unused tag members do not become match cases. malformed tags, nominal initializer conflicts, inactive/uninitialized access, readonly updates and narrow overflow retain checked diagnostics. untagged unions containing types remain invalid: metadata has no overlapping runtime layout.
|
||||
|
||||
fixed type arrays may also start with `undefined` inside type/type-array producers: `types [2]mut type := undefined`, then fill `types[index] = i16` in a loop. nested and whole-array updates preserve shape, mutability and independent initialization state in local copies; `.len` is available before elements initialize. reading an uninitialized type leaf, returning an incomplete array or passing one to a compile-time procedure diagnoses a read before initialization. empty shapes are valid, but every dimension must be explicit and nonnegative. construction charges each expanded cell against the shared execution bound, including nested zero-leaf shapes.
|
||||
|
||||
ordinary fixed value arrays use the same construction inside type/type-array producers: `counts [2]mut usize := undefined`, then fill indexed elements or replace whole rows/arrays. initialized leaves retain exact scalar, string, optional, record and tagged-union values; local copies retain independent partial initialization state. generated lengths/defaults and compile-time arguments can use initialized elements, while incomplete arrays cannot enter checked IR or procedure arguments. array `.len` and empty shapes do not read leaves. constant materialization of mutable slices or nonnull data pointers remains unsupported.
|
||||
|
||||
ordinary procedures also accept `undefined` as the initializer of a mutable local with a concrete runtime type, including nested records and fixed arrays. field/index writes initialize storage without reading its previous contents; aggregate copies retain independent contents and initialization state in the interpreter. ordinary compile-time calls can fill this storage and return materializable values, but observing or materializing an uninitialized leaf is diagnosed. native storage uses LLVM poison, not zero initialization: assign every leaf before observing it, and do not rely on native read-before-initialization traps for `undefined` storage. immutable bindings, globals, inferred array lengths and other expression uses remain invalid.
|
||||
|
||||
`some!(value)` constructs the present branch of an expected optional, evaluating its payload once and retaining exact payload typing. `value ??i16 :: some!(null)` is present with an absent inner optional; it differs from outer `null`. nested presence survives compile-time results, generic cache keys and generated defaults. optional-result procedures still require explicit returns on every path.
|
||||
|
||||
procedures returning fixed arrays or records/tuples containing `type`, such as `types proc() [2]type { return [i16, bool] }` or `config proc() struct { child type, count usize } { return { child = i16, count = 2 } }`, execute entirely at compile time. every argument is explicitly supplied, constant, cached and erased; nested/aliased results, empty shapes and dependent `$N` lengths retain exact shape and mutability. compile-time producers accept ordinary type-array/type-record inputs, and runtime generic procedures accept erased `$` parameters with these shapes. direct projections generate signatures, constructors, storage and defaults; explicit `$()` calls inside producers retain owned values. cache identity uses canonical type leaves without merging distinct nominal types, and copies cannot mutate cached inputs/results. imported diagnostics, shared type-control execution limits and repeatable compilation work through interpreter/native execution. ordinary parameters remain excluded from dependent signatures; use `$N` for lengths. runtime type-array/type-record storage and reflection 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 owned metadata arguments remain diagnostic; mutable slice parameters may borrow partially initialized backing to fill it.
|
||||
|
||||
local metadata storage uses owned compile-time snapshots alongside ordinary runtime parameters and effects. explicit annotations (`T type := i16`), inferred mutable type bindings (`T := i16`), metadata arrays/records/tuples, helper results and checked projections are erased before runtime checking. sequential updates, saved copies, static branches, bounded construction loops and scoped metadata cleanup select concrete runtime storage without emitting type values. runtime branches and loops may create independent local metadata; they cannot conditionally update an enclosing compile-time snapshot. runtime optional/collection captures and ordinary effects keep their execution semantics.
|
||||
|
||||
```hon
|
||||
compute proc(value i16) i16 {
|
||||
T := i8
|
||||
T = i16
|
||||
local T :: value
|
||||
return local
|
||||
}
|
||||
```
|
||||
|
||||
procedures whose metadata depends on ordinary parameters retain fully constant-input execution; every input to those calls remains explicitly supplied and constant. metadata parameters/results also retain their existing compile-time-only contract. nested storage, partial initialization, checked updates, recursion and fallible cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their annotation permits updates; mutable bindings still permit whole-value replacement. ordinary local aliases and scalar metadata projections keep their runtime-call contract.
|
||||
|
||||
readonly type-valued slices (`[]type` and slices of metadata records/arrays) cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them. local array/slice views retain live aliases with checked `usize` bounds, frozen subview offsets, indexing and `.len`; later backing writes or whole-array replacement remain visible, while by-value array/record copies stay independent. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity.
|
||||
|
||||
mutable metadata slice views (`[]mut type` and slices of metadata records/arrays) borrow writable array places, including nested fields/indices. immutable slice bindings may update their mutable elements; readonly backing and enclosing places never gain write access. compile-time helpers share aliases and commit writes to caller storage, including initialization and scoped `defer`/`errdefer` cleanup. calls with mutable borrowed arguments execute afresh rather than reusing cached effects; readonly-only inputs snapshot at call entry and retain content-based nominal caching. readonly results snapshot before return cleanup, and owned arguments/results require initialized leaves. expired local views, bounds errors and runtime-controlled metadata mutation diagnose. mutable results, metadata sentinels, data-pointer borrowing and pointer captures remain unsupported; metadata has no runtime storage.
|
||||
|
||||
optional `if`/`while` captures also execute in type-producing bodies: `if count |length| { return [length]i16 }` generates storage from an optional `usize`. ordinary checked IR validates each executed guard; present payloads become immutable owned constants with exact scalar/aggregate types and strings, and absent optionals skip the body. while guards observe updated locals on each iteration; captures stay body-local across break/continue and early return. generated lengths/defaults, imported producers, cache identity and repeated compilation preserve these values. null data/function pointers materialize as null; callbacks retain source declaration identity, while nonnull data pointers and unsupported slice types retain the constant-materialization escape boundary.
|
||||
|
||||
type-producing bodies also execute scalar/enum `match` statements and type-valued match expressions in initializers, assignments and returns, such as `return match wide { true: i16, false: i8 }`. selected expression arms may themselves be matches. an ordinary checked IR match returns the selected arm index, preserving subject/pattern typing, range boundaries, overlap, unreachable-arm and exhaustiveness diagnostics without checking unexecuted bodies. statement arms retain scoped locals, assignments, early returns and enclosing-loop break/continue; expression selection shares the type-control step bound.
|
||||
|
||||
matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, `return struct { values [2](match wide { true: i16, false: i8 }) }` generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. general type-valued execution remains open.
|
||||
|
||||
tagged-union matches in type-producing bodies support immutable by-value payload captures in statements, expressions and nested type/storage positions. for example, `match choice { .count |length|: return [length]i16, .empty: return bool }` uses a captured `usize` to generate storage. grouped variants require identical nonvoid payload types; invalid captures in unselected arms are still diagnosed. selected payloads become owned typed constants, preserving narrow integers, nested aggregates and strings across generated lengths/defaults, loop snapshots and repeatable compilation. active payloads containing data pointers or unsupported slice types retain the constant-materialization escape boundary below.
|
||||
|
||||
ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples, fixed arrays, tagged unions and explicit fallible results from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate and fallible arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned constants become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum/optional fields and IEEE float bits. tagged unions retain their actual discriminant and only the active payload, including nested aggregates and void/zero-cell variants. fallible constants retain success/error identity, composed or payload-bearing errors and void success; `catch` can consume them in generated lengths, defaults and runtime expressions.
|
||||
|
||||
computed callbacks materialize as references to their original source declarations, never as temporary interpreter function IDs. `choose proc() @proc(value i16) i16 { return increment }` can supply `$(choose())`, compile-time arguments and generated defaults. optional callbacks preserve absence and nested presence; arrays, records, tagged payloads and both fallible channels retain exact callback signatures and specialization-cache identity. imported private targets remain bound to their originating declarations, including across repeated compilation and native emission.
|
||||
|
||||
readonly byte-slice results (`[]u8` and `[;sentinel]u8` for any byte sentinel) snapshot their bytes into owned literals before evaluation storage is released. ordinary compile-time helpers can return string subviews, views of locally filled arrays or immutable global arrays, including nested records/arrays, tagged payloads, optionals and both fallible channels. explicit `$()` expressions, generic arguments and generated defaults preserve embedded NULs, arbitrary byte values, exact declared slice types and content-based specialization-cache identity. nonzero-sentinel snapshots retain an owned trailing sentinel outside the logical length; empty views also retain it. snapshots survive evaluation/session disposal and repeated compilation; uninitialized source bytes diagnose read-before-initialization, and sentinel bytes are rechecked after evaluation.
|
||||
|
||||
readonly slices of other materializable element types also own their snapshots, including distinct bytes, wider integers, arrays, records, callbacks and nested readonly slices. generated backing has module lifetime, so returning a computed slice from a runtime helper does not borrow its stack. exact element types, logical lengths, zero-sized elements, full-width signed/unsigned sentinels, nested optional/tagged/fallible channels and content-based specialization identity survive `$()`, compile-time arguments, generated defaults and repeated compilation. snapshots recheck source initialization, address ranges and trailing sentinels; recursive expansion is bounded to 100,000 values and 128 evaluation/serialization levels. mutable slices, nonnull data-pointer elements and address-bearing untagged union elements retain the escape diagnostic.
|
||||
|
||||
untagged union constants snapshot every byte of their native-layout storage, including padding and bytes beyond a smaller overlapping member. scalar reinterpretation, nested records/arrays, optionals, tagged payloads, fallible channels and generated defaults preserve exact nominal types and by-value copies through `$()` and compile-time arguments. specialization keys compare the full snapshot, not one selected field. every storage byte must be initialized. because untagged storage has no active-member identity, all member layouts must be free of data pointers, callbacks, strings and slices, recursively; otherwise materialization diagnoses an escape rather than retaining interpreter handles, even if a numeric member was initialized.
|
||||
|
||||
`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings, expression statements and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. nonnull data pointers, unsupported slice types and address-bearing untagged union layouts cannot escape temporary evaluation as generated constants; active payloads or aggregate fields containing unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, broader storage-bearing materialization and broader `meta` operations remain open.
|
||||
|
||||
`if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. fully bound type-specialized helper predicates also select guards implicitly when their expanded bodies are constant and effect-free; runtime inputs, mutation, mutable globals, foreign calls and explicit traps prevent implicit probing. only the selected body is specialized and checked; both bodies remain parsed and name-resolved. selected bodies retain scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. other ordinary calls stay runtime unless `$()` requests evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies.
|
||||
|
||||
generic guards also specialize side-effect-free `minval!`, `maxval!`, `sizeof!` and `alignof!` expressions with constant type operands.
|
||||
|
||||
`compile_error!(message)` requires a compile-time string and reports its owned text at the call site when that body is elaborated. unselected compile-time branches do not report it; ordinary runtime branches remain checked. messages from constant-input procedures retain their bytes through later evaluation, interpreter diagnostics and native executable emission.
|
||||
|
||||
`match $(subject) { ... }` selects an expression or statement arm before specializing or checking its body, including ordinary constant-input calls. concrete generic procedures also select matches on side-effect-free constant subjects, including substituted scalar/enum/tagged-union arguments and pure field/index projections. ordinary checked IR validates every pattern and capture declaration before selection; only the selected body is elaborated, while all bodies remain parsed and name-resolved. by-value payload captures retain exact types and owned strings, and selected bodies may use runtime parameters, nested matches, cleanup, returns and enclosing-loop transfers. ordinary call subjects, runtime parameters and mutable locals stay runtime, as do contextual aggregate bindings that might omit runtime defaults; ordinary nongeneric literal matches still check all arms. use `$()` to request explicit evaluation. unsupported storage-bearing payloads retain the escape boundary above.
|
||||
|
||||
type `==` and `!=` comparisons in compile-time expressions and specializations preserve aliases, nominal declaration identity and structural pointer/array/callback signatures. composed error-set identity and specialization caching ignore member ordering, grouping and repetition. for example, `read proc($T type, value T) i16 { if (T == i16) return value else return value.count }` checks the scalar or record path for each concrete call without requiring both operations on every `T`.
|
||||
|
||||
`field!(base, name)` selects a member using a compile-time string, without runtime reflection metadata. names may come from constants, constant-input procedures or erased generic parameters; only the name is evaluated at compile time, while the base retains ordinary selection semantics. records, tuples (canonical decimal names such as `"0"`), enum/error-set members, tagged payloads, callbacks and sequence `"len"`/`"ptr"` members reuse exact typing, single base evaluation, address-taking and writable-place checks. type-producing bodies also snapshot selector names for nested aggregate updates, preserving earlier by-value copies. unknown members, runtime/nonstring names and invalid calls are diagnosed; tagged payload reads still check the active field.
|
||||
|
||||
`typeinfo!(T)` returns the compile-time-only `TypeInfo` declared by `@std/meta`; import that exact path, optionally aliased, in the calling package. arrays expose `child` and `len`, distinct types expose their immediate backing type, and records/enums expose owned readonly `fields` collections. each field has `name`, exact `type` and ordinal `index`; enum indices follow declaration order, not numeric discriminants, and enum field types retain the reflected enum or composed error-set identity. records also expose their original optional `name`, `is_tuple` and `.auto`/`.c` layout; tuple names are canonical decimal ordinals. aliases preserve underlying names and nominal identities.
|
||||
|
||||
ordinary and inline metadata matches select bodies before runtime checking, including in nongeneric procedures. metadata collections support `.len`, checked indexing, by-value iteration and compile-time procedure inputs/results; copied field records can change without mutating the readonly collection. field names/types drive `field!`, generic calls, generated storage and defaults through interpreter/native execution, without runtime reflection objects or borrowed interpreter addresses. construction and expansion share the type-control bound. the existing `std/meta.EnumFieldStruct` and `std/enummap.EnumMap`, `init` and `get` work through interpreter/native execution, including sparse/aliased/composed enum keys, absent entries and aggregate copy isolation. generated-map arguments infer key/value types before checking contextual `.tag` arguments; conflicting rigid arguments and unresolved generics still diagnose. `get` explicitly returns `null` after its lookup. general storage-bearing constant materialization and runtime metadata storage remain unsupported.
|
||||
|
||||
`struct_type!(layout, names, types, defaults)` generates an ordinary record from constant fixed arrays or tuples of equal length. for example, `struct_type!(.auto, ["value", "bias"], [i16, i8], [null, 2])` requires a `value` initializer and supplies an `i8` default for `bias`. literal collections retain each field's context, including heterogeneous types and defaults; computed collections can come from helpers and loop-filled producer locals. names must be unique ASCII identifiers. each default is checked as `?Field`: absent means required, present supplies an owned field value. generated fields retain ordinary exact typing, aggregate copy isolation, field selection and checked updates through interpreter/native execution. producer specializations cache nominal identity; imported producers and repeated compilation retain source provenance and fresh owned values. incomplete arrays and runtime dependencies are rejected, and construction shares the type-control execution bound.
|
||||
|
||||
layout accepts `.auto` or `.c`, including constant enum members such as `Layout.c`. C-layout records must be nonempty; fields may contain concrete scalars, data pointers, optional data pointers, explicitly backed enums, nonempty fixed arrays and nested C-layout records. distinct types and non-C representations are rejected. this does not add aggregate C calling conventions or storage-bearing constant materialization.
|
||||
|
||||
callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage.
|
||||
|
||||
`true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
|
||||
|
||||
`!`, `and` and `or` require boolean operands and produce `bool`. prefix `!` and `-` bind tighter than arithmetic; equality binds tighter than `and`, which binds tighter than `or`. binary operators associate left to right, while prefixes nest right to left. operands evaluate left to right: `false and rhs` and `true or rhs` skip the right operand, including calls, lazy global initialization and runtime traps. both operands are still parsed, resolved and type-checked; errors in a skipped operand remain source diagnostics without forcing a runtime trap. this applies in initializers, assignments, returns, call arguments and conditions. [programs/boolean_conditions.hon](programs/boolean_conditions.hon) demonstrates combined predicates; `./build/honey run programs/boolean_conditions.hon` prints `true`.
|
||||
@@ -49,40 +201,137 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca
|
||||
|
||||
`name = expression` updates a local `:=` binding. `::` bindings, parameters, named functions, builtin types and globals are not assignment targets. the established binding type is preserved: assignment inputs do not widen or infer that type, though numeric literals can adopt its float context. function-valued locals accept replacement functions with the same parameter and result types.
|
||||
|
||||
compound updates `+= -= *= /= &= |= xor= <<= >>= <<|=` preserve the target type and ordinary writable-place checks. the target address and old value are evaluated once before the right operand, then the result is stored through that same address; `/=` is float-only and `xor=` must be contiguous. field/index/pointer targets, cleanup bodies, ordinary compile-time calls and type-producing locals use the same checked operations.
|
||||
|
||||
integer `~`, `&`, `xor` and `|` preserve exact operand types, including distinct integer identities; incompatible fixed integer types remain invalid. shifts `<<`, `>>` and saturating `<<|` preserve the left type and accept unsigned counts independently of its width. ordinary shifts reject known out-of-range counts and trap for runtime counts at least the left bit width. left shifts discard high bits; signed right shifts extend the sign. saturating shifts accept any count, preserve zero and clamp nonzero values to the signed minimum/maximum or unsigned maximum. shifts bind below arithmetic; `&`, `xor` and `|` share one left-associative level below shifts and above comparisons. parenthesize bitwise-OR header expressions before capture lists. type-valued `A | B` remains error-set composition.
|
||||
|
||||
fixed arrays use `[N]T` for readonly elements and `[N]mut T` for mutable elements; literals use `[a, b]`, including empty, nested and multiline forms with trailing commas. `values [4]mut i64 := [1, 2, 3, 4]` permits `values[2] = 10`. writable elements require a mutable local binding and `mut` at every traversed array level; parameters and globals remain readonly. whole-array assignment only requires a mutable binding, but source and destination types must match recursively, including counts and element mutability. initializations, assignments, arguments and returns copy arrays by value, including nested rows.
|
||||
|
||||
array lengths currently support nonnegative integer literals, unary signs, `+`, `-`, `*` and package-level immutable integer constants; local constants and other compile-time expressions are not implemented. `[_]` infers a binding's array length from its initializer. element annotations require concrete supported value types: use `i64` or `f64` rather than the `int` or `float` constraints. an unannotated array literal has readonly elements.
|
||||
array lengths support nonnegative integer literals, unary signs/complement, `+`, `-`, `*`, bitwise operators/shifts and package-level immutable integer constants; use `$()` for broader compile-time execution. `[_]` infers a binding's array length from its initializer. element annotations require concrete supported value types: use `i64` or `f64` rather than the `int` or `float` constraints. an unannotated array literal has readonly elements.
|
||||
|
||||
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`.
|
||||
|
||||
data pointers use `@T` for one value and `*T` for many values, with `mut` after the pointer marker for writable pointees. `&value` borrows addressable initialized storage; postfix `^` reads or writes the pointee. pointer mutability is independent of binding mutability: an immutable pointer binding or parameter can mutate its `mut` pointee. mutable pointers weaken to readonly pointers with the same child type. address-taking also accepts an expected many-pointer type, such as `pointer *mut i16 :: &values[0]`; arbitrary temporaries are not addressable.
|
||||
|
||||
pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, `.len`, and `.ptr`; `.ptr` returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require `usize` and caller-provided valid storage. many-pointer `pointer + offset` requires a `usize` offset, advances by the pointee's storage size and preserves its type/mutability; single-pointer addition, reversed addition and pointer subtraction are invalid, matching Bro. operands evaluate left to right once; one-past pointers can be formed but not dereferenced. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. `==` and `!=` compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as `<pointer>`. pointee lifetimes remain caller-managed; there is no borrow checker.
|
||||
|
||||
slices use `[]T` or `[]mut T` and borrow storage rather than copying elements. `values[start..end]` checks ordered, upper-exclusive `usize` bounds; either bound may be omitted for arrays, pointers to arrays and slices. many-pointer slices require an explicit end. empty and one-past slices are valid, but indexing at `.len` traps. `.ptr` returns a many-pointer and `.len` returns `usize`; copies, calls and aggregate storage preserve aliases. mutable slices can write through immutable bindings and parameters, while readonly views cannot strengthen implicitly. contextual `&[]` creates an empty mutable slice; borrowed backing storage must outlive every view.
|
||||
|
||||
sentinel slices use `[;sentinel]T` and checked construction such as `pointer[..length;0]`. the sentinel is a representable compile-time integer stored immediately after the logical elements, outside `.len`; construction checks both accessible bounds and its value. sentinel views can weaken to plain slices, not the reverse. string values expose readonly byte indexing, slicing, `.ptr` and `.len`, and convert to `[]u8` or `[;0]u8`; their module-owned backing includes an implicit NUL without changing the logical length.
|
||||
|
||||
optionals use `?T`; values of `T` and `null` initialize optional storage, parameters, results and aggregate fields. `== null` and `!= null` test presence. postfix `value?` unwraps or traps, while `if value |payload|` and `while value |payload|` bind a payload only inside the taken body; loop guards are re-evaluated and `continue` returns to the guard. optional data/function pointers use a nullable pointer representation; other optionals retain a separate presence flag and by-value payload. optional pointer weakening preserves aliases and readonly restrictions.
|
||||
|
||||
fallible values use explicit `T ! E` channels, where `E` is an enum, record or tagged union. success and error values initialize storage, arguments and returns; fallible values also work in arrays, records and callbacks. `void ! E` functions may fall through successfully. native unbacked enums and tagged unions compose with `A | B`, including grouped/imported error sets; compatible subsets widen without changing variant identities. immutable match subjects narrow within explicit/grouped arms and final `else` arms, allowing residual errors to propagate into smaller channels.
|
||||
|
||||
`try expression` evaluates once, extracts success or returns a compatible error from the enclosing fallible function. `expression catch [|err|] fallback` evaluates only the selected path; fallbacks may be expressions, yielding or terminal statement blocks or, for void success, blocks that fall through. inferred `T!` signatures are not implemented. write `builtin!(...)` without a gap before `!`; in general expressions a spaced `T ! (E)` denotes a fallible type.
|
||||
|
||||
`defer statement` and `errdefer [|err|] statement` register reached cleanup in the current block; braces introduce explicit nested scopes. cleanup runs LIFO when its scope exits, including return, yield, break and continue; returned and yielded values are evaluated and copied before cleanup. `errdefer` requires a fallible function and runs only on a language-error return while its scope remains active, not on a successful yield. cleanup cannot return, propagate with `try` or transfer control out of itself; a nested value block may yield locally. source errors remain ordinary values: catch does not intercept diagnostic traps or compiler allocation/IO failures, and traps do not unwind source cleanup. a fallible `main` prints its success value normally; an unhandled error prints nothing and exits with status `1`, without creating a diagnostic.
|
||||
|
||||
`sizeof!(T)` and `alignof!(T)` report the supported target's native layout, including aggregate padding, slice descriptors and nullable pointers. `ptrcast!(value)` takes its full result pointer type from context; `ptrcast!(T, value)` states it explicitly. casts preserve the storage location and optional presence, permit byte reinterpretation, and cannot remove readonly qualification. `constcast!` changes pointer or slice mutability without changing the pointee type; it does not make genuinely readonly storage writable or extend its lifetime. `anyopaque` is a pointer-only pointee, not a storable or dereferenceable value.
|
||||
|
||||
`memcopy!(destination, source)` copies matching typed slices or pointers to arrays, checking equal lengths and rejecting nonempty overlap. `memset!(destination, value)` repeats complete typed values, including aggregate fields, rather than repeating a byte. destinations require writable elements; empty and zero-sized copies/sets do not access storage. interpreter byte views synchronize with the original typed storage so casts and memory operations preserve aliases.
|
||||
|
||||
native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data/function pointers, nullable pointers and `void` results. `c_func` declarations and callbacks may end their parameter list with `...`; calls require the fixed prefix and promote trailing booleans and 8/16-bit integers to `c_int`. supported floating arguments are already `f64`; trailing aggregates and Honey string descriptors are not C ABI values. strings may initialize immutable byte pointers, including nullable C format parameters; pass `.ptr` explicitly for variadic strings. generated bindings may declare `c_struct` records, nominal pointer-only `opaque` types and `*c_func(...) Result` callback signatures. unused foreign declarations do not force ABI validation or native symbol emission; aggregate-by-value signatures remain unsupported. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations; `c_double` maps to `f64`, while demanded `c_float` and `c_longdouble` types remain unsupported. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported.
|
||||
|
||||
the existing `std/arraylist` and `std/mem` work without replacement bindings or library code. interpreter/native checks cover initialization, reserve/growth, append/pop/clear, allocation failure without losing the original list, zero-sized elements and deferred cleanup on error. native checks use the real `mem.c_allocator` and imported `ffi/c` declarations for aggregate element storage and over-aligned allocation/reallocation; interpreter allocation checks use a fixed-capacity test allocator, not foreign calls. broader metadata operations remain a later acceptance gate.
|
||||
|
||||
existing `std/hashmap.HashMap`, `AutoHashMap` and `StringHashMap` execute without replacement library code. checks cover collision chains, zero-hash normalization, growth/rehashing, duplicate-key errors, absent lookups, aggregate copy isolation, allocation failure preserving the map and deferred cleanup. native checks use the real C allocator for integer boundaries, booleans, sparse/aliased enums, nested distinct/array/record keys (ignoring record padding), content-based strings and custom callbacks; interpreter checks use separate fixed-capacity typed buffers. a declared result supplies the initializer parameters: `map hashmap.AutoHashMap(i16, i16) := hashmap.init(allocator)`; `StringHashMap` and custom `HashMap` results also infer their exact private/custom callbacks. explicit calls such as `hashmap.init(i16, i16, null, null, allocator)` remain supported.
|
||||
|
||||
existing `std/io.print(format, writer, args)` parses constant formats and expands tuple fields before runtime without adapting its implementation to the compiler. escaped braces, default bool/integer/string/enum formatting, nested scalar distinct values, `{s}`, `{d}`, `{b}`, `{o}`, `{x}`, `{X}` and `{c}` work through interpreter/native execution; malformed formats, wrong argument counts and unsupported types diagnose. native `f64` default/decimal/scientific (`{e}`) formatting uses the real imported variadic `snprintf`; that path retains its interpreter foreign-call trap. checks compare exact bytes with short-writing callbacks and cover write failures, zero progress and invalid callback counts.
|
||||
|
||||
constant-required contexts such as `inline for` iterables propagate evaluation demand through calls before eager body expansion. demanded execution follows reached control flow, including guarded `compile_error!`, and uses checked IR for borrowed local mutation such as `mem.set` and helper slice writes. snapshots preserve partially initialized caller storage and commit owned updated values; addresses cannot escape evaluation. runtime inline bodies suspend demand, and demanded/runtime instances have distinct cache identities. inferred literal-origin numeric locals retain contextual compile-time values until a rigid numeric source establishes their type; explicit annotations, casts and call results remain rigid.
|
||||
|
||||
single pointers to fixed arrays implicitly borrow matching slice storage without copying, respecting pointer/element mutability, exact nominal elements and sentinel weakening. optional coercions preserve absence and borrow only present payloads.
|
||||
|
||||
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}`.
|
||||
|
||||
enums use `Kind :: enum { number, empty }`, qualified members such as `Kind.number`, and contextual `.number` literals. unbacked enums use compilation-wide `u16` variant identities; explicit integer values require a backing, as in `Code :: enum(i16) { missing = -1, ready = 42 }`, and must increase within that backing's range. enum identities remain nominal across packages. equality/inequality and explicit backing extraction are supported; enum arithmetic and ordering are rejected. reserved keywords are valid enum and tagged-union member names.
|
||||
|
||||
`Id :: distinct u32` creates a nominal type with its backing's runtime representation. integer/float literals adopt distinct numeric context; arithmetic preserves the identity and checked-overflow rules. `Id(value)` explicitly constructs a value, applying checked conversion for numeric backing. nested distinct and aggregate backing require the immediate backing type; explicit extraction removes one layer. `Name :: alias declaration` preserves the original function/global/type identity, including imported declarations; aliases have no independent runtime storage.
|
||||
|
||||
untagged `Bits :: union { integer u64, real f64 }` overlays its members in shared storage. keyed construction selects exactly one member; reading another member reinterprets the same bytes. tagged unions use `union(enum)` or `union(Kind)`; an explicit enum must contain every union variant. construct them with `Choice { number = 42 }`, contextual `.number{42}`, `.point{x = 40, y = 2}` for record payloads, or `.empty` for `void` payloads. field reads/writes validate the active tag. nested aggregates copy by value, including across calls. CLI union results print `{payload}` (or `{}` for a void payload); untagged results print the first member.
|
||||
|
||||
`if (guard) then_value else else_value` is a boolean value expression. `if value |payload| then_value else else_value` instead tests optional presence and binds the exact payload as an immutable by-value capture in the then expression; nested optionals preserve present-null payloads. the guard runs once and only the selected expression executes; `else` is required and binds to the nearest conditional. both runtime branches must fit the result context, including optional/fallible channels and aggregate copies. constant-input procedures, direct `$()` evaluation, generic branch specialization, explicit compile-time subject selection and generated type positions reuse checked match execution. boolean guards require parentheses unless they are calls or explicit `$()` expressions; optional captures also permit bare guards. parenthesize a conditional used as a match-arm value to distinguish it from an `if` statement.
|
||||
|
||||
value blocks contain ordinary statements and produce a result with `yield expression`:
|
||||
|
||||
```hon
|
||||
answer i16 :: {
|
||||
value i16 := 40
|
||||
yield value + 2
|
||||
}
|
||||
```
|
||||
|
||||
`yield` exits the nearest enclosing value block, match arm or catch handler with a copied result. ordinary nested statement blocks and loops do not change its target; return, break and continue retain their usual targets. conditional branches also accept yielding blocks. result context reaches yielded expressions, including narrow integers, aggregates and optional/fallible channels. nonvoid fallthrough, incompatible yields and yields outside a value source diagnose; only reached invalid paths trap. constant evaluation and type-producing procedures support yielding blocks, generated storage/defaults and owned snapshots. empty, singleton and keyed record/tuple literals retain their existing syntax; statement-bearing braces select a value block. labeled yields and value-producing loops remain unsupported.
|
||||
|
||||
`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can yield results, return or transfer loop control; pointer captures belong to a later gate.
|
||||
|
||||
a final `inline |tag|:` arm on an enum, or `inline |payload[, tag]|:` on a tagged union, expands into one ordinary arm for each variant not covered earlier. dispatch remains runtime and evaluates the subject once. enum captures and optional union tags are immutable compile-time enum values; payloads are immutable by-value captures with each variant's exact type, including a `void` value for empty variants. either capture may be discarded with `_`. static tag matches/conditions select variant-specific operations before checking, including generic calls, nested inline arms and type-producing procedure bodies. ordinary payloads remain runtime values, and return/break/continue/deferred cleanup retain ordinary match semantics. redundant/nonfinal inline arms, invalid captures and scalar/untagged subjects are diagnosed. expansion shares the 4096-step type-control bound.
|
||||
|
||||
composed error sets use the same inline arms, including through `catch |err| match err { ... }`. sets containing only void variants use `inline |tag|:`; sets with any payload use `inline |payload[, tag]|:`, with void values for their empty members. nested compositions, transparent aliases and repeated members expand each remaining variant once. named struct errors use their declaration name as the variant tag, and payloads retain their exact struct type and by-value isolation. for example, `Missing | union(enum) { failed i8 }` can use `inline |payload, tag|: match tag { .failed: i16(payload), .missing: 0 }`. constant inputs and type-producing bodies also select generated storage from composed payloads. pointer captures and metadata reflection remain open.
|
||||
|
||||
`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' integer family (`int` or `usize`) 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 integer.
|
||||
a literal `while true` without a reachable break is terminal, including when only nested loops break. nonvoid functions need no fallthrough return after it; runtime guards and loops with reachable breaks still require a return.
|
||||
|
||||
range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. current execution supports direct integer ranges only; first-class range values, collection iteration, pointer captures and index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`.
|
||||
`unreachable` is an operand-free statement that traps only when reached, preserving its source location in interpreted, native and compile-time execution. untaken and unused occurrences do not diagnose. it is a fatal trap, not a source error value or a cleanup transfer; it does not execute deferred cleanup.
|
||||
|
||||
`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.
|
||||
|
||||
range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. first-class range values and range pointer/index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`.
|
||||
|
||||
collection loops accept arrays, slices, strings and single pointers to arrays: `for values |value, index| { ... }`. element captures are immutable by-value copies with the exact element type; optional indices have type `usize`, and either capture may be discarded with `_`. `|@value|` borrows an element through a single pointer, writable only when the source storage permits mutation. the iterable and its length are evaluated once, then each iteration reads its current element: array lvalues are borrowed, temporary arrays are retained once, and slice rebinding does not change the active loop's view. empty collections execute no body, including arrays of zero-cell elements. collection loops share range-loop cleanup, break/continue and return handling through the interpreter and native backend.
|
||||
|
||||
`break` exits the nearest enclosing `for` or `while`; `continue` restarts a `while` condition or advances a `for` range through its guarded update. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`.
|
||||
|
||||
name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `i64`, `f64`, `usize`, `bool`, `string`, `void`); sibling scopes may independently reuse names.
|
||||
name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names; sibling scopes may independently reuse names.
|
||||
|
||||
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its 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.
|
||||
the driver uses `@source/compilation` to own loaded source buffers, copied paths, one AST arena, the package graph and a shared identifier pool. `add_source` transfers its `ArrayList(u8)` allocation only on success, resetting the caller's list; infrastructure failure leaves the bytes caller-owned and rolls back the source registration, partial AST and source diagnostics. interned names may remain in the shared pool. `deinit` releases all retained frontend storage. metadata borrows are invalidated by registration growth, but source/package/node IDs remain stable within their compilation.
|
||||
|
||||
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.
|
||||
AST `NodeId` values are compilation-wide, so declarations in different files cannot alias. spans carry their originating `diagnostics.SourceId`; `ast.source_files` exposes borrowed paths and text. `compilation.compile` resolves each package independently, checks the complete arena and lowers one independently owned IR module, selecting an entry only from the requested root package. interpreter diagnostics and generated native traps retain the originating filename and local line/byte-column. borrowed `ast.view` trees must be refreshed after builder mutation and must not be passed to `ast.deinit`.
|
||||
|
||||
AST nodes retain a compact 16-byte layout on the bootstrap target: a kind, three 32-bit payload words and one packed flag byte in existing padding. file/package-local, foreign-callable, variadic and opaque metadata therefore need neither per-node boolean fields nor another allocation. spans and variable-length payloads remain in parallel storage; cloning and replacement copy the flags with the node.
|
||||
|
||||
the AST renderer displays nested blocks, cleanup bodies/captures and compound operators alongside ordinary statements.
|
||||
|
||||
identifier interning is compilation-owned: initialize a `strpool.StringPool`, pass it to `lexer.scan` and `sema.resolve` before the diagnostic store, and release it after lowering. scanning interns identifier spellings once per pool; parsing preserves their IDs in compact AST storage, exposed through `ast.name_id`, while retaining source spans for diagnostics. name resolution keys scopes by those IDs and interns builtin names into the same pool, so identifier hashing and equality do not scan spelling bytes. IDs are meaningful only within their originating pool; independent compilations may assign the same numeric ID to different names. interning failures propagate as infrastructure errors rather than producing an invalid identifier ID. IR retains no pool dependency.
|
||||
|
||||
compiler token/operator mappings, scalar type metadata and backend spellings use compile-time `@std/enummap` tables directly; state-dependent layout and control-flow dispatch remain ordinary code. IR exports `integer_minima` and `integer_maxima` maps for shared integer bounds, with absent entries for noninteger types. `enummap.init_default(E, V, storage_default, fields)` initializes unused value slots so sparse maps can become owned compile-time constants without changing which keys are present. `enummap.get` still performs runtime linear lookup; static construction is not a constant-time lookup guarantee.
|
||||
|
||||
resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put`, `get` and `deinit` with custom `HashMap` and `StringHashMap`. automatic keys support integers, booleans, enums, distinct types, fixed arrays and records recursively; record padding is ignored. slices, floating-point values, pointers, optionals and unions require explicit hashing and equality through `HashMap` (`StringHashMap` supplies content-based byte-string operations). keys and values are stored by value; referenced resources remain caller-owned. the builtin declaration count is inferred, and builtin ID storage derives its fixed stack-array length from that declaration array rather than repeating a count or initializer.
|
||||
|
||||
`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, pointer/container/record/enum descriptors and construction mappings; its result retains an AST borrow for callable-signature compatibility queries. query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit` before releasing the AST. `ArrayType.kind` distinguishes fixed arrays, slices, optionals and fallibles; `element` records their element/success type, `error_type` the fallible error channel, with mutability and sentinel metadata for slices. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays, slices, optionals, fallibles and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result, success channel or tagged-union payload, not as a parameter or standalone stored value.
|
||||
|
||||
function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage.
|
||||
|
||||
the checker keeps union-find parent links and group sizes in a compact array separate from per-node inference metadata. root walks and path compression access only that link storage; both temporary arrays are released before `check.check` returns.
|
||||
|
||||
numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt a supported numeric family only when their value is exactly representable; float literals remain float and opt into ordinary IEEE rounding. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int`/`i64`, `usize`, and `float`/`f64` values do not implicitly convert between families. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid. `value f64 :: 9007199254740993` is diagnosed, including when float context arrives through aliases, division, or array elements; `9007199254740993.0` explicitly permits rounding. exactly representable integers beyond the signed/unsigned integer ranges remain valid in float context. explicit numeric casts are not yet implemented.
|
||||
numeric types include `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `isize`, `usize` and `f64`. on the supported 64-bit target, `isize` and `usize` are signed and unsigned 64-bit types. current `int`/`i64`, `uint`/`u64` and `float`/`f64` annotations share their runtime types; other concrete types remain distinct even when their widths coincide. integer literals adopt numeric context through arithmetic, inferred bindings, arrays and calls before undemanded integers default to `int`. fixed values never convert implicitly between types. integer arithmetic checks the selected type's bounds; unsigned negation is invalid. `/` requires and supplies float context. implicit integer-to-float literal context requires exact representation: `value f64 :: 9007199254740993` is diagnosed, while `9007199254740993.0` permits IEEE rounding.
|
||||
|
||||
the conversion policy is value-preserving by default: any future implicit widening must preserve every value of the source type, not merely the value observed at one use. narrowing, potentially inexact integer-to-float conversion, float-to-integer conversion, and signedness changes that cannot preserve the full source range require an explicit operation. integer arithmetic remains checked rather than wrapping; floating arithmetic retains IEEE rounding.
|
||||
|
||||
explicit numeric conversion uses `T(value)`, such as `u8(index)`, `usize(byte)` or `f64(count)`, with exactly one numeric argument. integer conversions trap with `integer arithmetic overflow` if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including `f64(9007199254740993)`. operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above `i64`'s maximum defaults to `u64`. boolean conversions, arbitrary-width integers, `f32` and implicit typed-value widening are not implemented. native storage uses LLVM `i8`/`i16`/`i32`/`i64`, including array elements and function parameters/results.
|
||||
|
||||
`minval!(T)` and `maxval!(T)` return exact concrete integer bounds, including distinct integer types. `divtrunc!(left, right)` requires matching integer types and truncates toward zero, with ordinary contextual literal inference and single left-to-right operand evaluation. integer division by zero and signed minimum divided by `-1` trap before executing host/LLVM division; `/` remains floating-point division. these builtins also work in checked compile-time execution.
|
||||
|
||||
`rem!(left, right)` requires matching integer or floating types and evaluates operands once, left to right. integer remainders follow the dividend's sign; signed minimum remainder `-1` is zero. floating remainders use truncating IEEE remainder. zero divisors, including floating negative zero, trap before native division/remainder. distinct integer identity and checked compile-time execution are preserved.
|
||||
|
||||
character literals contain one Unicode scalar value, such as `'A'` or `'é'`, and use contextual integer typing. escapes support `\0`, `\n`, `\r`, `\t`, `\\` and `\'`; invalid UTF-8, multiple characters, unknown escapes and unterminated literals diagnose. destination integer widths still constrain code points.
|
||||
|
||||
`@std/strconv.parse_float(allocator, text)` parses a complete borrowed `[]u8` decimal into `f64`. it accepts an optional sign, decimal point, and exponent; whitespace, embedded NUL bytes, trailing input, hexadecimal, infinities, and NaNs return `invalid_number`. finite subnormals and signed zero are preserved; overflow and nonzero values rounded to zero return `out_of_range`. allocation failures propagate as `out_of_memory`, and temporary storage is always released. the Darwin backend uses libc's C-locale conversion without changing the process locale; rounding follows the caller's floating-point rounding mode. numeric lowering uses this interface, keeping exact integer-to-float validation compiler-specific.
|
||||
|
||||
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed.
|
||||
|
||||
`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. union descriptors additionally retain overlapping byte layouts and per-variant discriminants. typed construction, field/element addresses and load/store instructions use storage reserved per invocation; nested copies and accesses reuse that storage without per-iteration allocation. internal `address` values remain distinct from source `pointer` values: explicit bridge instructions preserve initialization checks, while pointer offsets retain typed element strides. native pointers occupy eight bytes; interpreter handles intern root storage identity and byte offset across frames, including union serialization. match lowering uses ordinary branches and local result slots; enum and distinct identities are checked before erasure to their runtime backing.
|
||||
|
||||
`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.
|
||||
IR construction owns derived layout metadata. `ir.add_array`, `ir.add_record` and `ir.add_union` check shape capacity and compute their interpreter storage and native layouts; record/union shapes own copied field metadata. all aggregate kinds use `ArrayId` shapes and the existing backing segments. `ir.add_parameter` and `ir.add_local` create slots with disjoint aggregate backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, aggregate-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated aggregate results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required.
|
||||
|
||||
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. native code generation and compound assignment remain unimplemented.
|
||||
`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.
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# self-hosting
|
||||
|
||||
- [ ] milestone: Honey compiles its production compiler, then that compiler compiles itself again and builds/runs the example programs.
|
||||
|
||||
## current boundary
|
||||
|
||||
directory packages support file-local import namespaces and postfix declaration
|
||||
selection through the interpreter and native backend. concrete 8/16/32/64-bit
|
||||
integers, checked numeric conversions, records/tuples, construction/defaults,
|
||||
enums, distinct/alias types, overlapping untagged/tagged unions, field reads/writes
|
||||
and exhaustive statement/value matching work through both execution paths.
|
||||
source data pointers/references support aliasing, mutation, calls and aggregate
|
||||
storage; explicit callback signatures, many-pointer addition, mutable/sentinel
|
||||
slices, optionals and layout/cast/memory builtins work through both execution paths.
|
||||
native scalar/pointer `c_func` declarations include variadic calls and C integer
|
||||
promotions; executed foreign calls remain diagnostic traps in the interpreter.
|
||||
explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO
|
||||
`defer`/`errdefer` run through both paths; source errors are values, not diagnostic
|
||||
traps or compiler infrastructure failures.
|
||||
cached generic specialization and type-producing procedures support explicit/inferred
|
||||
compile-time arguments and generated record/array storage through both paths.
|
||||
ordinary procedures can compute scalar, nested record/array, tagged-union,
|
||||
fallible, callback and readonly-slice compile-time values using mutable locals, loops and recursive calls;
|
||||
explicit `$()` expressions use the same constant evaluation path.
|
||||
readonly `[]u8` and `[;sentinel]u8` snapshots own exact bytes and any byte sentinel after evaluation.
|
||||
readonly typed slices snapshot materializable elements into module-lifetime backing,
|
||||
including nominal bytes, arrays/records, callbacks, nested slices and integer sentinels.
|
||||
address-free untagged unions snapshot every initialized storage byte, including padding.
|
||||
mutable slices, nonnull data pointers and address-bearing untagged union layouts
|
||||
retain the constant-materialization escape boundary.
|
||||
compile-time `if`/`match` selection and type comparisons specialize only the selected body,
|
||||
including owned by-value tagged payload captures in ordinary generic procedures.
|
||||
type-producing procedures also accept ordinary
|
||||
constant inputs, cached and erased like explicit compile-time arguments. mutable
|
||||
type/value locals, direct and aggregate field/index assignments, branches,
|
||||
scalar/enum/tagged-union matching and bounded integer-range/collection/while loops now generate types and storage. optional
|
||||
if/while captures and tagged payload matches select nested constructors, lengths, signatures and defaults;
|
||||
array/slice/string/pointer-to-array collection loops support exact copy/pointer
|
||||
captures and `usize` indices through both paths; type-producing bodies iterate
|
||||
materializable arrays/strings by value without retaining interpreter addresses.
|
||||
type-valued fixed arrays support nested copies, indexed/whole updates,
|
||||
checked projections and by-value collection captures in type-producing bodies;
|
||||
`undefined` type/value arrays and type-valued records/tuples track initialization
|
||||
per leaf while field/index updates fill generated types/defaults.
|
||||
type-array and type-record parameters/results cross compile-time procedure boundaries
|
||||
with owned snapshots, cached nominal identity and erased arguments.
|
||||
initialized type-valued records/tuples support schemas/defaults, nested
|
||||
field/index updates and owned copies/captures inside compile-time producers.
|
||||
optional type-valued storage preserves nested presence, owned metadata payloads,
|
||||
checked unwraps and immutable captures across compile-time procedure boundaries.
|
||||
fallible type-valued channels own their active payloads, propagate checked errors,
|
||||
and select generated storage through `try`/`catch` and scoped cleanup.
|
||||
tagged type-valued unions own active payloads across compile-time calls and erased
|
||||
arguments; checked payload updates preserve saved copies, and whole-value assignments
|
||||
initialize undefined storage or select another variant.
|
||||
readonly type-valued slices own nested metadata leaves across compile-time calls,
|
||||
by-value iteration and erased arguments. local metadata slice views borrow live
|
||||
array places; mutable views and helper parameters update shared backing, while
|
||||
readonly results snapshot before cleanup. metadata data-pointer borrowing remains open.
|
||||
ordinary metadata-input procedures also compute owned runtime values, including
|
||||
reflected field names/counts, recursive helpers and fallible cleanup.
|
||||
explicit and inferred local metadata storage also coexist with ordinary runtime
|
||||
parameters and effects; parameter-dependent metadata retains constant-input execution.
|
||||
`field!` selects record/tuple/enum and sequence members using compile-time names,
|
||||
including runtime places and type-control updates without runtime reflection.
|
||||
`struct_type!` generates ordinary/C-layout records from constant name/type/default
|
||||
arrays or tuples, including loop-filled producer storage and owned defaults.
|
||||
`inline for` expands constant ranges, fixed value/type arrays, heterogeneous
|
||||
value/type tuples and strings with immutable exact captures and compile-time-selected exits.
|
||||
final inline match arms expand nominal enums, tagged unions and composed error sets
|
||||
with static tags and exact by-value payloads, preserving runtime dispatch and scoped control flow.
|
||||
`typeinfo!` exposes owned compile-time array/distinct/record/enum metadata, including
|
||||
readonly field collections, exact nominal types and reflected generated storage/defaults.
|
||||
the existing `meta.EnumFieldStruct` and `enummap.EnumMap`, `init` and `get` work;
|
||||
metadata data-pointer borrowing, broader materialization and full library acceptance remain open.
|
||||
`some!` preserves nested optional presence; ordinary procedures initialize concrete
|
||||
runtime `undefined` locals and compute fully initialized materializable constants.
|
||||
the existing `ArrayList` uses nested generic inference, file-local helpers and
|
||||
the real imported C allocator through native execution; interpreter checks use
|
||||
non-FFI allocators and cover zero-sized storage, growth, failure and cleanup.
|
||||
existing hash maps execute collision chains, rehashing, duplicate/absent lookups,
|
||||
aggregate copies and allocator failure/cleanup through both paths; native checks
|
||||
cover automatic integer/bool/enum/distinct/array/record keys, content-based strings
|
||||
and custom callbacks with the real C allocator. expected result types infer initializer
|
||||
types/callbacks, including automatic, string and custom maps.
|
||||
existing `io.print` expands constant formats and tuple fields, with exact boolean,
|
||||
integer, string, enum and distinct formatting through both paths. native float
|
||||
formatting uses imported `snprintf`; interpreter float formatting retains the FFI trap.
|
||||
character literals, multiline conditional branches, partial compile-time argument
|
||||
prefixes and checked `rem!` support the formatter without a replacement library.
|
||||
`unreachable` is a demanded source trap; terminal literal-true loops need no return.
|
||||
`compile_error!` retains owned user messages and skips unselected compile-time bodies.
|
||||
boolean and optional-capture `if ... else` expressions reuse checked matches for
|
||||
runtime/constant values, contextual optional/fallible results and generated types.
|
||||
yielding value blocks, match arms and catch handlers copy results before scoped
|
||||
cleanup and preserve enclosing function/loop transfers through both execution paths;
|
||||
compile-time blocks also generate types, storage and defaults.
|
||||
compound updates evaluate places once, and integer bitwise/truncating/saturating
|
||||
shifts preserve widths and nominal types through runtime and compile-time execution.
|
||||
the driver retains frontend storage and source provenance through
|
||||
`@source/compilation`, with compilation-wide AST node identities.
|
||||
file commands retain single-file behavior; directory commands compile the
|
||||
package's production files and load its dependencies in separate scopes.
|
||||
|
||||
## completed milestone: scalar-function packages
|
||||
|
||||
verified: compile and run a small multi-file directory package importing a second
|
||||
package of scalar functions, through both the interpreter and native backend.
|
||||
using the existing standard library is a later acceptance gate: even `std/mem`
|
||||
needs records, pointers, generics and C calls.
|
||||
|
||||
- [x] retain source/package identity and source ownership across the compilation; keep one identifier pool and unambiguous declaration IDs.
|
||||
- [x] load directory packages once per canonical path, with deterministic file discovery, package scopes and root-only entry selection. follow Bro's existing relative and project-root `@` import rules; diagnose cycles rather than recurse indefinitely.
|
||||
- [x] finish postfix namespace selection and resolve file-local import aliases without injecting dependency names into the importing package; bare and aliased import declarations are already parsed for dependency discovery.
|
||||
- [x] reuse ordinary scalar signature checking, function values and IR calls for selected declarations; namespaces have no runtime representation.
|
||||
- [x] preserve originating filenames and spans in dependency diagnostics and native runtime traps; retain demand-driven diagnostic recovery.
|
||||
- [x] verify sibling source files, transitive/shared imports, same-spelled declarations in different packages, alias conflicts, missing packages/members, argument errors and dependency-file diagnostics. preserve existing single-file commands.
|
||||
|
||||
visibility follows Bro's public-by-default contract; `@hide:file` isolates private
|
||||
declarations from sibling files and import selection. bare `@hide` and
|
||||
`@hide:package` retain sibling access but reject imports. unknown attributes diagnose.
|
||||
the scalar-function milestone did not include record fields or imported types;
|
||||
those now work for the record/tuple slice.
|
||||
test discovery and FFI remain outside these completed slices.
|
||||
|
||||
## subsequent dependency gates
|
||||
|
||||
- [x] concrete data models: required integer widths/conversions, records/tuples and construction, fields, enums, distinct/alias types, unions and matching. examples: `source/lexer/token.hon`, `source/sema/resolve.hon`, `std/mem/mem.hon`.
|
||||
- [x] native integer widths (`i8`–`i64`, `u8`–`u64`, `isize`/`usize`) and checked explicit numeric conversions, including float truncation/range checks and integer-to-float rounding. preserve exact types through arrays, calls and range captures; retain strict implicit conversion rules.
|
||||
- [x] records/tuples, typed/contextual/anonymous construction, field defaults and reads/writes, nested aggregate value copies, imported record types and constructors through interpreter/native execution.
|
||||
- [x] nominal enums and distinct types, transparent declaration/type aliases, overlapping untagged/tagged unions, contextual payload construction, active-field checks and exhaustive scalar/enum/tagged-union matching through interpreter/native execution. pointer captures, inline specialization and yielding value blocks remain with their respective later gates; error-set composition is covered below.
|
||||
- [x] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`.
|
||||
- [x] source data pointers/references: `@T`/`*T`, `mut`, address-taking and dereference, pointer-aware fields/indexing, array `.len`/`.ptr`, readonly weakening, location equality, recursive pointer-linked records and shared pointees across interpreter/native calls and aggregate copies.
|
||||
- [x] function-pointer signatures: explicit `@proc(...) Result`, structural signature checking, callbacks in parameters/results/records/arrays, vtable calls and imported signature aliases; many-pointer `+ usize` arithmetic preserves pointee type, mutability, stride and location identity through interpreter/native execution.
|
||||
- [x] mutable/sentinel slices with checked bounds, borrowed aliases and string views; optional values/pointers/callbacks, null checks, unwrap and conditional/loop captures; target layout, pointer/const casts and typed memory copy/set through interpreter/native execution. scalar/pointer C ABI declarations build/run allocator callbacks using `malloc`, `realloc`, `free` and `posix_memalign`; arbitrary interpreter FFI, generic standard-library compilation and native startup/IO remain with later gates.
|
||||
- [x] fallible results and cleanup: explicit `T ! E`, error-set composition, `try`/`catch`, `defer`/`errdefer`, error captures and matched error residuals through interpreter/native execution; infrastructure errors remain distinct from diagnostic traps. fallible `void` may complete on fallthrough; unhandled entry errors exit with status `1`. inferred `T!` signatures and yielding catch blocks remain with inference/value-control-flow work. examples: `source/main.hon`, `source/sema/check/check.hon`.
|
||||
- [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage.
|
||||
- [x] specialization checkpoint: explicit/inferred type and integer/boolean/string/callback/null value parameters, cached concrete runtime calls, nominal type-producing procedures, generated record/array storage and recursive data pointers through interpreter/native execution. preserve declared value types, source diagnostics and repeatable compilation; runtime values cannot supply compile-time arguments.
|
||||
- [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] 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.
|
||||
- [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. aggregate field/index updates and type-valued arrays are covered below; cleanup and type-valued record storage remain open.
|
||||
- [x] type-match checkpoint: type-producing bodies execute scalar/enum match statements and direct type-valued match expressions in initializers, assignments and returns, including nested selected arms. ordinary checking/IR execution select the arm and validate subject/pattern types, ranges, overlap, unreachable arms and exhaustiveness; only the selected body is elaborated. generated storage, scoped locals, loop transfers, early returns, nominal cache identity, selected-body diagnostics, dependency provenance and repeatable compilation work through interpreter/native execution. all bodies remain parsed/name-resolved and execution shares the type-control bound.
|
||||
- [x] nested type-match checkpoint: matches select types inside constructors, array lengths, generic arguments, local annotations, callback signatures and field defaults in type-producing bodies. selection precedes ordinary expansion, preserves literal spelling/source provenance and loop snapshots, and caches nominal results before expanding recursive fields. selected-body and pattern diagnostics, failed deferred elaboration, imported producers and repeatable compilation work through interpreter/native execution; deferred selection shares the producer's type-control bound.
|
||||
- [x] tagged/fallible constant checkpoint: explicit `$()` expressions, generic value parameters and type-procedure inputs materialize tagged unions and both fallible channels through interpreter/native execution. preserve active tags/payloads, nested raw aggregate projections, string ownership, void/zero-cell variants, composed/payload errors, declared channel types, normalized cache identity and repeatable compilation. `catch` computes generated lengths and runtime values. address-free untagged unions are covered below; unsupported active payloads remain escape diagnostics rather than retaining temporary interpreter handles.
|
||||
- [x] tagged type-match checkpoint: type-producing bodies execute tagged-union matches with immutable by-value payload captures in statement, expression and nested storage positions. ordinary match checking validates every arm's captures, including grouped variants; selected payloads retain exact scalar/aggregate types and owned strings. generated storage/defaults, loop snapshots/transfers, cache identity, selected-body diagnostics, imported producers, dependency provenance and repeatable compilation work through interpreter/native execution. storage-bearing payloads retain the constant-materialization escape boundary.
|
||||
- [x] generic match-specialization checkpoint: side-effect-free constant scalar/enum/tagged-union subjects in concrete generic procedures and explicit `match $(subject)` expressions/statements select an arm before body specialization/checking. reuse ordinary checked IR for pattern types, ranges, overlap, exhaustiveness and every arm's capture validation; only the selected body is elaborated. exact by-value payload types/owned strings, runtime parameters, nested expression matches, cleanup, return/loop transfers, imported producers and repeatable compilation work through interpreter/native execution. ordinary call/runtime/mutable subjects stay runtime; contextual aggregates with possible runtime defaults are not implicitly evaluated. all bodies remain parsed/name-resolved; storage-bearing payloads retain the constant-materialization escape boundary.
|
||||
- [x] optional type-control checkpoint: type-producing bodies execute optional if/while guards with immutable by-value payload captures. ordinary checked IR validates guards; selected scalar/aggregate payloads retain exact types and owned strings across nested captures, generated storage/defaults, loop updates/transfers and early returns. absent optionals skip unselected bodies; invalid guards, capture mutation/shadowing, overflow and execution bounds retain diagnostics. imported producers, dependency provenance, nominal cache identity and repeatable compilation work through interpreter/native execution. null data/function pointers materialize correctly; nonnull storage-bearing payloads retain the constant-materialization escape boundary.
|
||||
- [x] aggregate type-update checkpoint: type-producing bodies update initialized, materializable aggregate locals through nested record fields and mutable fixed-array indices. ordinary checking validates assignments, retaining exact value types, mutability (including immutable payload captures), index types/bounds, unknown-field and narrow-overflow diagnostics. new owned constants preserve earlier by-value copies while later loops, generated storage and defaults observe updates. imported producers, dependency provenance, nominal cache identity and repeatable compilation work through interpreter/native execution. pointer/slice-backed mutation and partial record construction remain open; type-valued records/arrays and uninitialized value arrays are covered below.
|
||||
- [x] collection-iteration checkpoint: arrays, slices, strings and single pointers-to-arrays support immutable exact-type copy captures, storage-qualified pointer captures and optional `usize` indices through interpreter/native execution. iterables/bounds evaluate once, array lvalues remain borrowed and later element updates are visible; temporary arrays, aggregate copy isolation, zero-cell/empty collections, nested loops and cleanup across transfers retain ordinary semantics. ordinary constant-input procedures reuse the same checked IR; type-producing bodies iterate materializable arrays/strings with by-value captures, fixed entry-time place indices, generated storage, nominal cache identity, dependency diagnostics, shared execution bounds and repeatable compilation. storage-bearing type-control captures retain their existing boundary; type-valued elements are covered below.
|
||||
- [x] type-array storage checkpoint: type-producing bodies own initialized fixed arrays of type values, including explicit/inferred nested arrays, `.len`, checked `usize` indexing, whole/indexed updates and immutable by-value collection captures. persistent snapshots preserve copies and exact shape/mutability while borrowed array places observe later updates. selected types generate nested storage, constructors, signatures and defaults through interpreter/native execution, retaining selected-body and index/type/mutability diagnostics, imported provenance, nominal cache identity, shared bounds and repeatable compilation. uninitialized type arrays and local type-valued records are covered below; reflection remains open.
|
||||
- [x] type-array procedure checkpoint: fixed arrays of type values cross compile-time procedure inputs/results, including nested/aliased results, dependent `$N` lengths, empty shapes and erased `$types` parameters in runtime generic procedures. all inputs to type/type-array producers are explicit constants; owned snapshots preserve by-value isolation, exact shape/mutability and canonical/nominal cache identity. projections generate signatures, constructors, storage and defaults through interpreter/native execution. malformed inputs/results, runtime dependencies, imported provenance, shared execution bounds and repeatable compilation retain diagnostics and isolation. runtime type-array storage, type-valued record procedure boundaries and reflection remain open.
|
||||
- [x] field-selection reflection checkpoint: `field!(base, name)` evaluates/erases a constant string name and reuses ordinary selection through interpreter/native execution. records/tuples, enum/error-set members, tagged payloads, callbacks, pointer-qualified places and sequence `.len`/`.ptr` retain exact types, single base evaluation, address-taking, mutability and active-field checks. type-producing bodies snapshot changing selector names for nested aggregate updates without losing by-value isolation. runtime/nonstring names, malformed calls, missing members and noncanonical/overflowing tuple ordinals retain diagnostics; full metadata reflection and inline expansion remain open.
|
||||
- [x] uninitialized type-array checkpoint: fixed type arrays in type/type-array producers start with `undefined` and fill through loop-driven indexed, nested and whole-array updates. persistent local copies retain independent initialization state; `.len` works before leaves initialize. premature leaf reads and incomplete procedure arguments/results diagnose read-before-initialization without caching invalid values. explicit nonnegative dimensions, empty shapes, ordinary mutability/shape checks, nominal identity and repeatable compilation work through interpreter/native execution. construction shares the type-control bound per expanded cell, including nested zero-leaf shapes. ordinary value-array construction, local type-valued records, runtime undefined storage and metadata reflection are covered below.
|
||||
- [x] uninitialized value-array checkpoint: ordinary fixed arrays in type/type-array producers start with `undefined`, fill nested indices or replace whole arrays/rows, and generate storage/defaults through interpreter/native execution. owned snapshots preserve independent partial initialization in local copies; initialized scalar/string/optional/record/tagged-union elements retain exact types and ordinary checked operations. premature leaf reads and incomplete procedure arguments diagnose read-before-initialization. empty shapes, `.len` before initialization, mutability/type/index/bounds/overflow diagnostics, per-cell construction limits and repeatable compilation are verified. runtime undefined storage, ordinary value-returning procedures and partial type-valued records are covered below; storage-bearing constant materialization remains open.
|
||||
- [x] type-record storage checkpoint: initialized records/tuples containing type values live inside type/type-array producers, including local schemas/defaults, typed/contextual/inferred construction, nested records/arrays, checked field/index reads and updates, whole-value replacement and immutable by-value collection captures. persistent snapshots isolate copies/captures while borrowed array places observe later updates; type leaves retain nominal identity and ordinary value leaves reuse checked IR. generated storage/defaults, invalid field/type/default diagnostics, readonly places, narrow overflow, imported producers/provenance, cached nominal results and repeatable compilation work through interpreter/native execution. field validation/construction shares the type-control bound. procedure inputs/results, partial record initialization, metadata reflection and inline expansion are covered below; runtime type-valued records remain open.
|
||||
- [x] type-record procedure checkpoint: records/tuples containing type values cross compile-time procedure inputs/results, including nested arrays/records, aliases, defaults and dependent annotations. compile-time producers accept ordinary constant inputs; runtime generics accept erased `$` metadata parameters. owned snapshots isolate argument/result copies, exact type/value leaves retain checked diagnostics, and normalized cache keys preserve nominal generated types. generated storage/defaults, imported producers/provenance, shared execution bounds and repeatable compilation work through interpreter/native execution. partial record initialization, reflection and inline expansion are covered below; runtime metadata storage remains open.
|
||||
- [x] generated-record checkpoint: `struct_type!(layout, names, types, defaults)` builds ordinary records from constant fixed arrays or tuples with unique ASCII field names, concrete types and contextual optional defaults. literal heterogeneous collections, helper results and loop-filled producer arrays preserve exact types, owned defaults/copies and cached nominal identity through interpreter/native execution. `.auto` permits empty records; `.c` requires nonempty C-compatible fields and retains native field order/layout. malformed inputs, incomplete arrays, runtime dependencies, field/default validation, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and isolation. metadata inspection, inline expansion, aggregate C calling conventions and storage-bearing defaults remain open.
|
||||
- [x] inline-loop checkpoint: `inline for` expands constant integer ranges, fixed arrays (including type-valued arrays) and strings into separately checked runtime bodies through interpreter/native execution. exact immutable captures, `usize` collection indices, narrow inclusive maxima, selected conditional/tagged-match bodies, static break/continue tail pruning, nested inline/ordinary loops, runtime returns/errors and scoped cleanup retain ordinary semantics. empty loops skip body elaboration; runtime-dependent inputs/transfers, pointer captures, storage-bearing escapes and shared expansion bounds retain diagnostics. imported producers/provenance and repeatable compilation are verified. heterogeneous tuples and inline-match syntax are covered below; metadata iteration remains open.
|
||||
- [x] heterogeneous inline-tuple checkpoint: `inline for` iterates constant tuples with exact per-field value/type captures and `usize` indices through interpreter/native execution. literal/helper/aliased tuples, erased generic inputs, nested tuple projections, generated storage, empty shapes, static exits, cleanup and aggregate copy isolation retain existing semantics. type-producing bodies borrow tuple places so later fields observe updates while saved copies remain isolated. runtime dependencies and storage-bearing escapes are rejected before iteration, including discarded captures and early exits; named records and ordinary tuple loops remain invalid. imported producers, nominal type leaves and repeatable compilation are verified. inline-match syntax is covered below; metadata collections remain open.
|
||||
- [x] nominal inline-match checkpoint: final `inline |tag|:` enum arms and `inline |payload[, tag]|:` tagged-union arms expand remaining variants through interpreter/native execution. runtime subjects evaluate once; immutable static enum/tag captures specialize nested conditions, matches and generic calls while exact by-value scalar/aggregate/string/void payloads retain runtime semantics and copy isolation. statement/expression arms, ordinary loop/optional/match captures, scoped cleanup and return/break/continue work; type-producing bodies select generated storage and update outer type/value locals, including collection captures. invalid subjects/captures, redundant/nonfinal arms, ordinary pattern checks, imported provenance, shared expansion bounds and repeatable compilation retain diagnostics and ownership. composed error-set inline arms are covered below; pointer captures and metadata reflection remain open.
|
||||
- [x] composed inline-match checkpoint: composed error sets expand remaining variants through interpreter/native execution, including nested/repeated/aliased enum, tagged-union and named struct members. all-void sets use one static tag capture; payload-bearing sets use exact immutable by-value payloads and optional static tags, including void members. catch captures, nested inline tag matches, generic calls, owned aggregate copies, deferred cleanup and error returns retain ordinary semantics. constant inputs and type-producing bodies select generated storage with cached nominal identity; struct variant names remain identifiers in generated constants rather than embedding declarations. invalid compositions/captures, narrow payload types, redundant arms, imported dependencies and repeatable compilation retain diagnostics and ownership. member discovery shares the type-control bound; pointer captures and metadata reflection remain open.
|
||||
- [x] type-metadata reflection checkpoint: `typeinfo!(T)` uses the actual `@std/meta` ABI to classify supported types and expose array child/length, immediate distinct backing, record names/tuple/layout and enum/record fields through interpreter/native execution. readonly owned metadata collections retain exact nominal/type leaves, ordinal indices, composed enum identity, checked projections, by-value iteration/copies and compile-time procedure boundaries. ordinary/inline matches select runtime bodies; reflected fields drive `field!`, generics and generated storage/defaults. arity/type/import/ABI failures, match/capture validation, bounds, readonly updates, runtime escape rejection, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and ownership. existing `meta.EnumFieldStruct` and `enummap.EnumMap` generated storage are verified without replacement library code; full `enummap.init/get` is covered below, while general type-valued execution and broader constant materialization remain open.
|
||||
- [x] enum-map acceptance checkpoint: existing `enummap.init/get` run through interpreter/native execution with sparse/aliased/composed enum keys, absent entries and owned aggregate values. contextual `.tag` and aggregate arguments reuse key/value types inferred from rigid generated-map arguments; unresolved generics and rigid conflicts still diagnose. `some!` constructs exact, single-evaluation optional payloads and preserves nested present-null values through compile-time results, cache keys and generated defaults. concrete mutable runtime locals initialize from `undefined`, including nested record/array updates, pointer access and by-value copies; ordinary compile-time procedures materialize fully initialized storage and reject incomplete snapshots. native undefined storage is poison, not zeroed or per-leaf checked. `get` now explicitly returns `null` on lookup fallthrough, preserving the ordinary nonvoid return contract.
|
||||
- [x] array-list acceptance checkpoint: existing `std/arraylist` and `std/mem` initialize, reserve/grow, append/pop/clear and release ordinary/aggregate/zero-sized storage without replacement library code. nested generic inference canonicalizes generated types through pointers and derives exact slice `.ptr` types; rigid conflicts remain diagnostic. `minval!`/`maxval!` preserve integer types and checked `divtrunc!` traps on zero and signed overflow through interpreter/native execution. file-local helpers retain real `@hide:file` visibility, newline `else` preserves match default arms, and repeated `_` parameters remain sinks. generated C records, nominal opaque pointers and foreign callbacks load from existing `ffi/c`; unused unsupported signatures neither diagnose nor emit external symbols, while demanded unsupported ABI/types diagnose. interpreter fixed-capacity allocator checks cover failure without losing storage and deferred error cleanup; native `mem.c_allocator` checks cover aggregate growth and over-aligned allocation/reallocation at `-O0`/`-O2`. variadic C calls and formatted IO are covered below; arbitrary interpreter FFI, aggregate C calls and broader metadata acceptance remain open.
|
||||
- [x] boolean conditional-expression checkpoint: `if (guard) then_value else else_value` reuses canonical matches through interpreter/native execution. guards evaluate once and only the selected expression executes; nested conditionals bind `else` to the nearest conditional. exact result types, aggregate copy isolation, contextual optional/fallible channels, direct `$()` values, constant-input procedures, generic branch selection and nested generated type positions retain existing checked execution. missing `else`, nonboolean guards and incompatible runtime branch types diagnose. optional captures, yielding value blocks, compound updates and bitwise operators are covered below.
|
||||
- [x] optional conditional-expression checkpoint: `if value |payload| then_value else else_value` reuses canonical presence matches through interpreter/native execution. single-evaluation guards and lazy branches preserve exact immutable by-value captures, nested optional presence, aggregate isolation and runtime pointer/callback/slice/zero-sized payloads. contextual optional/fallible results, constant-input procedures, generated types/defaults and explicit compile-time subject selection retain checked execution and owned payload snapshots. nonoptional guards, capture scope/shadowing, malformed captures, missing `else`, incompatible branches and compile-time overflow retain diagnostics. yielding value blocks are covered below; broader constant materialization remains open.
|
||||
- [x] yielding-block checkpoint: value blocks and `yield expression` produce exact/contextual results through interpreter/native execution, including conditional branches, match arms and catch handlers. nested value sources select the nearest target; ordinary blocks/loops preserve it. yielded aggregates snapshot before LIFO cleanup, successful yields skip `errdefer`, and return/error/break/continue retain enclosing targets. ordinary constants, generic branch specialization and type-producing blocks generate owned values, types, storage and defaults. missing operands/results, incompatible yields, capture mutation and outward cleanup transfers retain diagnostics and demand-driven traps; imported provenance and repeatable compilation are verified. labeled yields and value-producing loops remain outside this checkpoint.
|
||||
- [x] compound/bitwise checkpoint: arithmetic and bitwise compound updates evaluate mutable local/field/index/pointer places and their old values once before the RHS through interpreter/native execution. integer complement, and/or/xor, truncating left shifts, signed/unsigned right shifts and saturating left shifts preserve widths and distinct identity; unsigned counts retain independent types. known invalid counts diagnose even in untaken runtime branches; dynamic counts trap without native LLVM poison. checked arithmetic, float-only division, readonly diagnostics, scoped cleanup, ordinary constants, generated storage, generic branch selection and repeatable compilation retain existing semantics. type-valued `|` remains composition while runtime `|` and capture delimiters retain their grammar. full hash-map acceptance remains open.
|
||||
- [x] hash-map execution checkpoint: existing `std/hashmap.HashMap`, `AutoHashMap` and `StringHashMap` run collision chains, zero-hash normalization, growth/rehashing, duplicate-key errors, absent lookups and owned aggregate values without replacement library code. interpreter checks use separate typed allocation buffers and preserve the original map on allocation failure, including cleanup; native `mem.c_allocator` checks cover all integer widths/boundaries, booleans, sparse/aliased enums, nested distinct/array/record keys with padding ignored, content-based byte strings and custom callbacks at `-O0`/`-O2`. specialization retains `usize` collection indices, nominal/explicit pointer-cast result types, rigid slice borrowing/string context and source-known optional callbacks; ordinary checking rejects incompatible elements and readonly reversal. `unreachable` demand-traps with provenance through both paths and compile-time execution; literal-true loops distinguish escaping and nested breaks. `compile_error!` owns computed string diagnostics and skips unselected compile-time bodies. initializer inference from expected result types is covered below.
|
||||
- [x] expected-result inference checkpoint: omitted compile-time type/value arguments infer from rigid runtime arguments and expected results before contextual literal defaults. typed bindings, assignments, returns, ordinary/nested calls, aggregate fields/elements/defaults, optional/fallible channels, callbacks, match arms, yielding/catch blocks, arithmetic/bitwise operands and explicit `$()` expressions retain exact checked types through interpreter/native execution. generated nominal results infer counts and private/custom callback identities; existing `hashmap.init(allocator)` initializes `AutoHashMap`, `StringHashMap` and custom `HashMap` without replacement library code. rigid conflicts, unrelated nominal generators and unresolved arguments diagnose; explicit calls and fully explicit type-producer inputs retain their contracts. owned specializations and repeat compilation are verified.
|
||||
- [x] computed-callback materialization checkpoint: ordinary compile-time calls return source-bound callbacks through interpreter/native execution, including optional/nested presence, arrays/records, tagged payloads and both fallible channels. temporary IR function IDs map back to exact source declarations before evaluation storage is released. computed/direct aggregate arguments share specialization-cache identity and generated defaults; imported private targets, same-spelled functions, signature diagnostics, runtime trap provenance and repeated compilation retain their contracts. nonnull data pointers, unsupported slice types and address-bearing untagged union layouts retain their escape boundary.
|
||||
- [x] readonly byte-slice materialization checkpoint: ordinary compile-time calls snapshot readonly `[]u8` and zero-terminated `[;0]u8` results into owned literals before evaluation storage is released. string subviews, locally filled arrays, immutable global arrays and raw aggregate views preserve exact bytes, embedded NULs, declared slice types and normalized content-based cache identity through explicit `$()` expressions, generic inputs, generated defaults, nested records/arrays, tagged payloads, optionals and both fallible channels. imported helpers, session/source disposal and repeated compilation retain ownership through interpreter/native execution; uninitialized source bytes diagnose read-before-initialization and invalid ranges are rejected before dereference. nonzero sentinels and nonbyte/nominal-byte slices are covered below; mutable slices, nonnull data pointers and address-bearing untagged union layouts remain unsupported.
|
||||
- [x] nonzero-sentinel byte-slice checkpoint: readonly `[;sentinel]u8` compile-time results own exact payload bytes and any byte sentinel through interpreter/native execution. canonical string-backed views preserve logical lengths, empty views, embedded NULs, high bytes, computed/direct specialization-cache identity, generated defaults, inline captures, nested aggregates, optionals and both fallible channels. sentinel checks observe final evaluation storage, diagnosing mutation, uninitialized bytes and invalid addresses; imported helpers, source/session disposal and repeated compilation retain ownership. nonbyte/nominal-byte slices are covered below; mutable slices, nonnull data pointers and address-bearing untagged union layouts retain the escape boundary.
|
||||
- [x] untagged-union materialization checkpoint: address-free untagged unions own full native-layout byte snapshots through interpreter/native execution, including scalar/IEEE reinterpretation, nested padded records/arrays, smaller overlapping updates, zero-sized storage, distinct members and generated nominal unions. explicit `$()` values, compile-time arguments, full-byte specialization-cache identity, generated defaults, nested optionals/tagged payloads and both fallible channels preserve exact types and independent copies. uninitialized tail/padding bytes diagnose read-before-initialization; every member layout is checked recursively to reject data pointers, callbacks, strings and slices rather than retain interpreter handles. imported helpers/diagnostics, source/session disposal and repeated compilation retain ownership. address-bearing union layouts and broader constant materialization remain open.
|
||||
- [x] readonly typed-slice materialization checkpoint: ordinary compile-time calls snapshot readonly slices of materializable elements into module-lifetime backing through interpreter/native execution. nominal bytes, wider integers, arrays/records, source-bound callbacks, nested readonly slices, empty/zero-sized elements and full-width signed/unsigned sentinels retain exact types and logical lengths. explicit `$()` values, computed/direct content-based specialization identity, generated defaults and nested optional/tagged/fallible channels preserve ownership across helper returns, imported private declarations, source/session disposal and repeat compilation. initialization, address-range, sentinel, mutable-slice and nonnull-pointer escape diagnostics remain checked; 100,000-value/128-level snapshot bounds reject oversized and cyclic storage. mutable slices, nonnull data pointers and address-bearing untagged union layouts remain open.
|
||||
- [x] uninitialized type-record checkpoint: compile-time records/tuples containing type values start with `undefined` and fill through nested field/index updates or whole-value replacement. persistent snapshots preserve independent partial copies; defaults do not initialize undefined fields. initialized leaves and empty shapes work while premature type/value reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results supply erased generic inputs, generated storage/defaults and cached nominal types through interpreter/native execution. ordinary mutability/schema validation and shared per-field/cell construction bounds remain checked; general type-valued execution and broader materialization remain open.
|
||||
- [x] optional type-storage checkpoint: `?type` and optional type-valued records/arrays use owned compile-time snapshots, including nested `some!(null)`, contextual payload construction, mutable whole-value/field/index replacement, immutable if/while/conditional captures, null comparisons and checked unwraps. constant-input procedures and erased generic arguments preserve specialization-cache identity and nominal leaves while generating runtime storage through interpreter/native execution. undefined presence, incomplete payloads, non-type values, capture mutation and runtime dependencies retain diagnostics. imported private nominal types, session/source disposal and repeat compilation preserve ownership. fallible type-valued channels are covered below; general type-valued execution and broader constant materialization remain open.
|
||||
- [x] fallible type-storage checkpoint: explicit success/error channels containing types use owned compile-time snapshots, including nested optional/record/array payloads, whole-value updates, independent copies, constant-input procedures and erased generic inputs. `try` validates error subsets and propagates through enclosing producers; `catch` binds immutable exact error payloads and selects generated types or runtime handlers. composed metadata errors preserve named variants, nominal leaves and cache identity. returned snapshots precede scoped LIFO `defer`/`errdefer` cleanup; fallible `void` supports bare returns and successful fallthrough. incomplete/uninitialized channels, invalid payloads/compositions, capture mutation and runtime dependencies retain diagnostics. imported private types, source/session disposal and repeat compilation preserve ownership through interpreter/native execution. runtime metadata storage, general type-valued execution and broader 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] 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 support constant-input execution without metadata inputs or results. nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse type-control execution through interpreter/native paths. uninitialized leaves and runtime dependencies diagnose; imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. independent runtime parameters are supported by the mixed-execution checkpoint below.
|
||||
- [x] inferred metadata-local execution checkpoint: inferred mutable type bindings and metadata array/record/tuple locals support 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; 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. independent runtime parameters and metadata-local entry points are supported below.
|
||||
- [x] formatted-IO acceptance checkpoint: existing `std/io.print` parses constant formats and expands tuple fields through interpreter/native execution without replacement or adapted library code. escaped braces, all integer bases/boundaries, booleans, strings/arrays, sparse/aliased enums, nested scalar distinct values, runtime arguments and computed formats retain exact output; short writes, zero progress, invalid callback counts and write errors retain checked propagation. malformed formats, wrong tuple/count/type inputs and unsupported specifiers diagnose. native `f64` default/decimal/scientific formatting uses imported variadic `snprintf` at `-O0`/`-O2`, while interpreter FFI remains a demanded trap. character literals preserve Unicode code points and byte escapes; newline-separated conditional branches and package-local visibility match Bro. partial compile-time prefixes infer remaining arguments without overriding explicit values; pure type builtins and effect-free bound type helpers specialize guards. `rem!` preserves exact numeric types, signed-minimum remainder and checked zero divisors. immutable C byte-pointer coercions, borrowed pointer-to-array slices and optional presence retain mutability/nominal checks. inline iterables propagate constant demand through original parser calls, metadata indexing and borrowed initialization; guarded errors follow reached control flow, owned snapshots commit local mutation, contextual constant numeric locals preserve rigid boundaries, and runtime bodies/effects remain runtime. general type-valued execution, broader materialization and broader `meta` acceptance remain open.
|
||||
- [x] mixed metadata-local execution checkpoint: ordinary and generic procedures retain runtime scalar/pointer inputs and effects alongside erased explicit/inferred type, array and record locals. owned snapshots preserve sequential field/index/whole updates, saved copies, generated runtime storage, constant branches, bounded construction loops, static exits and scoped metadata cleanup. runtime branches/loops create independent metadata locals; optional/collection captures keep their runtime bindings. constant tagged/optional metadata selection specializes only the selected body. runtime-dependent metadata still requires fully constant-input execution, and runtime paths cannot mutate enclosing metadata snapshots. interpreter/native `-O0`/`-O2`, imported private nominal leaves, source/session disposal and repeat compilation retain ownership and diagnostics. mutable metadata borrowing and broader constant materialization remain open.
|
||||
- [x] mutable metadata slice-borrowing checkpoint: local type-valued array/slice views retain live aliases, frozen subview bounds and exact nominal leaves. mutable views update nested metadata fields/indices through immutable slice bindings, share helper parameters, initialize undefined backing and run scoped `defer`/`errdefer`; ordinary array/record copies remain independent. readonly results snapshot before return cleanup, readonly-only calls retain content-based specialization identity, and mutable borrowed calls never reuse cached effects. mixed runtime parameters/effects remain runtime; runtime-controlled metadata mutation, readonly reversal, expired places, incomplete owned snapshots and bounds/index failures diagnose. imported private leaves and repeat compilation/source disposal retain ownership through interpreter/native execution. metadata data pointers, pointer captures, mutable result materialization and sentinels remain open.
|
||||
- [ ] remaining gate: metadata data-pointer borrowing and pointer captures, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations. 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`.
|
||||
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
|
||||
- [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support.
|
||||
- [ ] self-hosting proof: bootstrap-built Honey builds the production compiler (excluding test-only imports); that executable rebuilds the compiler; the rebuilt compiler passes representative interpreter/native examples and CLI/error scenarios without Bro at runtime.
|
||||
|
||||
the gates may require interleaving (notably allocation, errors and generics).
|
||||
extend checking, IR and native emission together; accepting syntax alone does
|
||||
not complete a gate. additional targets, a package registry, a replacement
|
||||
linker and interpreter execution of arbitrary FFI are not prerequisites.
|
||||
+4
-3
@@ -10,7 +10,7 @@ these guidelines apply to Honey programs, the compiler, and the standard library
|
||||
|
||||
## bindings and types
|
||||
|
||||
- prefer `::` for bindings that stay fixed; use `:=` for bindings that change, including values mutated through pointers.
|
||||
- prefer `::` when the bound value stays fixed; use `:=` when the bound value changes. mutation through a mutable pointer or slice does not require rebinding and is not, by itself, a reason to use `:=`.
|
||||
- infer types when the initializer makes the intended type clear. write an explicit type when it determines representation, mutability, or a public contract.
|
||||
- request mutable pointers and slices only when the operation needs to modify their contents.
|
||||
- give aggregates meaningful defaults. use `undefined` only when every value that can be read is assigned first.
|
||||
@@ -20,8 +20,8 @@ these guidelines apply to Honey programs, the compiler, and the standard library
|
||||
- use four-space indentation and put opening braces on the declaration or control-flow line.
|
||||
- use single-line bodies for simple operations. use braces for multiple statements or nested control flow.
|
||||
- break long parameter lists and aggregate initializers across lines, with a trailing comma on the last item.
|
||||
- when breaking boolean or arithmetic expressions across lines, put continuation operators at the start of each line. indent operands one level inside the enclosing parentheses and put the closing parenthesis on its own line.
|
||||
- wrap multiline `if` and `while` guards in parentheses, with the opening body brace after the closing parenthesis, so the guard's end is visually distinct from its body.
|
||||
- when breaking boolean or arithmetic expressions across lines, put continuation operators at the start of each continued line. inner groups may keep opening parentheses beside their first operand and combine closing parentheses to avoid staircase indentation.
|
||||
- wrap multiline `if` and `while` guards in parentheses. put the outer guard's operands on following lines, indented one level, and its closing `)` on a separate line. a simple single-statement body may follow `)` on that same line without braces; otherwise put the opening body brace there.
|
||||
|
||||
```hon
|
||||
debug.assert(
|
||||
@@ -44,6 +44,7 @@ while (
|
||||
```
|
||||
|
||||
- use early returns for terminal cases so the main path stays shallow.
|
||||
- use grouped `match` arms for boolean enum membership; reserve compile-time enum maps for enum-to-value data mappings.
|
||||
- register `defer` cleanup immediately after acquiring a resource. use `errdefer` for cleanup needed only until ownership is transferred successfully.
|
||||
|
||||
## allocation and ownership
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Darwin arm64 uses the 64-bit inode dirent layout from sys/dirent.h.
|
||||
DIR :: opaque
|
||||
dirent :: c_struct {
|
||||
d_ino c_ulonglong
|
||||
d_seekoff c_ulonglong
|
||||
d_reclen c_ushort
|
||||
d_namlen c_ushort
|
||||
d_type c_uchar
|
||||
d_name [1024]c_char
|
||||
}
|
||||
|
||||
opendir c_func(path ?*c_char) ?*mut DIR
|
||||
readdir c_func(directory ?*mut DIR) ?*mut dirent
|
||||
closedir c_func(directory ?*mut DIR) c_int
|
||||
|
||||
# Darwin arm64 struct stat from sys/stat.h; timestamps use the existing timespec binding.
|
||||
Stat :: c_struct {
|
||||
st_dev c_int
|
||||
st_mode c_ushort
|
||||
st_nlink c_ushort
|
||||
st_ino c_ulonglong
|
||||
st_uid c_uint
|
||||
st_gid c_uint
|
||||
st_rdev c_int
|
||||
st_atimespec timespec
|
||||
st_mtimespec timespec
|
||||
st_ctimespec timespec
|
||||
st_birthtimespec timespec
|
||||
st_size c_longlong
|
||||
st_blocks c_longlong
|
||||
st_blksize c_int
|
||||
st_flags c_uint
|
||||
st_gen c_uint
|
||||
st_lspare c_int
|
||||
st_qspare [2]c_longlong
|
||||
}
|
||||
stat c_func(path ?*c_char, result ?*mut Stat) c_int
|
||||
@@ -0,0 +1,16 @@
|
||||
# hand-written Darwin ABI declarations; pid_t is c_int in the existing bindings.
|
||||
# Darwin's spawn action and attribute handles are pointers to opaque records.
|
||||
posix_spawn_file_actions_t :: alias ?*mut anyopaque
|
||||
posix_spawnattr_t :: alias ?*mut anyopaque
|
||||
|
||||
posix_spawnp c_func(
|
||||
pid ?*mut c_int,
|
||||
file ?*c_char,
|
||||
actions ?*posix_spawn_file_actions_t,
|
||||
attributes ?*posix_spawnattr_t,
|
||||
arguments ?*?*mut c_char,
|
||||
environment ?*?*mut c_char,
|
||||
) c_int
|
||||
|
||||
# borrowed process environment; neither the vector nor its strings may be freed.
|
||||
_NSGetEnviron c_func() ?*mut ?*mut ?*mut c_char
|
||||
+1591
-82
File diff suppressed because it is too large
Load Diff
+105
-1
@@ -1,7 +1,12 @@
|
||||
import "@std/mem"
|
||||
import "@std/testing"
|
||||
import "@source/strpool"
|
||||
import "@source/diagnostics"
|
||||
|
||||
exposes_typed_declaration_views test {
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
value_id :: try strpool.intern(&strings, "value")
|
||||
builder := init_builder(mem.c_allocator, "value :: 42")
|
||||
defer deinit_builder(&builder)
|
||||
|
||||
@@ -11,6 +16,7 @@ exposes_typed_declaration_views test {
|
||||
&builder,
|
||||
Span{ start = 0, end = 11 },
|
||||
value_name,
|
||||
value_id,
|
||||
null,
|
||||
.constant,
|
||||
value,
|
||||
@@ -23,6 +29,7 @@ exposes_typed_declaration_views test {
|
||||
try testing.expect_equal(1, package_decl_count(&tree))
|
||||
decl :: package_decl(&tree, 0)
|
||||
try testing.expect_equal(DeclKind.statement, decl_kind(&tree, decl))
|
||||
try testing.expect_equal(value_id, name_id(&tree, decl))
|
||||
|
||||
statement :: statement_decl(&tree, decl)
|
||||
try testing.expect_equal(value_name.start, statement.name.start)
|
||||
@@ -33,12 +40,16 @@ exposes_typed_declaration_views test {
|
||||
try testing.expect_equal("value", source_text(&tree, statement.name))
|
||||
}
|
||||
|
||||
|
||||
exposes_array_type_literal_and_index_views test {
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
element_id :: try strpool.intern(&strings, "int")
|
||||
builder := init_builder(mem.c_allocator, "")
|
||||
defer deinit_builder(&builder)
|
||||
span :: Span{ start = 0, end = 0 }
|
||||
count :: try add_literal(&builder, .int, span)
|
||||
element :: try add_identifier(&builder, span)
|
||||
element :: try add_identifier(&builder, span, element_id)
|
||||
row :: try add_array_type(&builder, span, count, element, false)
|
||||
matrix :: try add_array_type(&builder, span, null, row, true)
|
||||
elements [2]NodeId :: [count, element]
|
||||
@@ -55,6 +66,7 @@ exposes_array_type_literal_and_index_views test {
|
||||
try testing.expect_equal(count, array_type(&tree, row).count?)
|
||||
try testing.expect(!array_type(&tree, row).mutable)
|
||||
try testing.expect_equal(element, array_type(&tree, row).element)
|
||||
try testing.expect_equal(element_id, name_id(&tree, element))
|
||||
try testing.expect_equal(ExprKind.array_literal, expr_kind(&tree, literal))
|
||||
try testing.expect_equal(2, array_element_count(&tree, literal))
|
||||
try testing.expect_equal(count, array_element(&tree, literal, 0))
|
||||
@@ -64,3 +76,95 @@ exposes_array_type_literal_and_index_views test {
|
||||
try testing.expect_equal(literal, index_expr(&tree, indexed).base)
|
||||
try testing.expect_equal(count, index_expr(&tree, indexed).index)
|
||||
}
|
||||
|
||||
registers_borrowed_sources_and_removes_failed_source test {
|
||||
builder := init_builder(mem.c_allocator, "")
|
||||
defer deinit_builder(&builder)
|
||||
first :: try set_source(&builder, "first.hon", "first")
|
||||
root :: try add_package(&builder, Span{ start = 0, end = 5, source = first }, &[])
|
||||
failed :: try set_source(&builder, "failed.hon", "failed")
|
||||
try testing.expect_equal(diagnostics.SourceId(1), failed)
|
||||
remove_source(&builder)
|
||||
try testing.expect_equal(first, builder.source_id)
|
||||
try testing.expect_equal("first", builder.source)
|
||||
next :: try set_source(&builder, "next.hon", "next")
|
||||
try testing.expect_equal(failed, next)
|
||||
# package roots may use a source other than the currently active source.
|
||||
declarations [1]NodeId :: [root]
|
||||
combined :: try add_package(&builder, Span{ start = 0, end = 5, source = first }, declarations[..])
|
||||
tree :: view(&builder, combined)
|
||||
try testing.expect_equal(first, node_span(&tree, combined).source)
|
||||
try testing.expect_equal("first", source_text(&tree, node_span(&tree, combined)))
|
||||
try testing.expect_equal(2, source_files(&tree).len)
|
||||
try testing.expect_equal("first.hon", source_files(&tree)[usize(first)].path)
|
||||
try testing.expect_equal("next.hon", source_files(&tree)[usize(next)].path)
|
||||
try testing.expect_equal("next", source_files(&tree)[usize(next)].text)
|
||||
try testing.expect_equal(1, builder_package_decl_count(&builder, combined))
|
||||
try testing.expect_equal(root, builder_package_decl(&builder, combined, 0))
|
||||
}
|
||||
|
||||
removes_only_registered_source_without_allocating_builder test {
|
||||
builder := init_builder(mem.c_allocator, "")
|
||||
defer deinit_builder(&builder)
|
||||
_ = try set_source(&builder, "failed.hon", "failed")
|
||||
remove_source(&builder)
|
||||
try testing.expect_equal(diagnostics.SourceId(0), builder.source_id)
|
||||
try testing.expect_equal("", builder.source)
|
||||
replacement :: try set_source(&builder, "replacement.hon", "")
|
||||
try testing.expect_equal(diagnostics.SourceId(0), replacement)
|
||||
}
|
||||
|
||||
|
||||
owns_generated_literal_text_across_finish test {
|
||||
tree Tree := undefined
|
||||
{
|
||||
builder := init_builder(mem.c_allocator, "")
|
||||
defer deinit_builder(&builder)
|
||||
source :: try set_source(&builder, "generated.hon", "input")
|
||||
at :: Span{ start = 0, end = 5, source = source }
|
||||
zero :: try add_integer(&builder, at, 0)
|
||||
maximum :: try add_integer(&builder, at, maxval!(u64))
|
||||
enabled :: try add_boolean(&builder, at, true)
|
||||
disabled :: try add_boolean(&builder, at, false)
|
||||
try testing.expect_equal(source, builder.source_id)
|
||||
try testing.expect_equal("input", builder.source)
|
||||
declarations [4]NodeId :: [zero, maximum, enabled, disabled]
|
||||
root :: try add_package(&builder, at, declarations[..])
|
||||
tree = finish(&builder, root)
|
||||
}
|
||||
defer deinit(&tree)
|
||||
zero :: package_decl(&tree, 0)
|
||||
maximum :: package_decl(&tree, 1)
|
||||
enabled :: package_decl(&tree, 2)
|
||||
disabled :: package_decl(&tree, 3)
|
||||
try testing.expect_equal(ExprKind.literal_int, expr_kind(&tree, zero))
|
||||
try testing.expect_equal("0", literal_text(&tree, zero))
|
||||
try testing.expect_equal("18446744073709551615", literal_text(&tree, maximum))
|
||||
try testing.expect_equal(ExprKind.literal_bool, expr_kind(&tree, enabled))
|
||||
try testing.expect_equal("true", literal_text(&tree, enabled))
|
||||
try testing.expect_equal("false", literal_text(&tree, disabled))
|
||||
try testing.expect_equal(1, source_files(&tree).len)
|
||||
try testing.expect_equal(diagnostics.SourceId(0), node_span(&tree, zero).source)
|
||||
try testing.expect_equal(0, node_span(&tree, zero).start)
|
||||
try testing.expect_equal(5, node_span(&tree, zero).end)
|
||||
try testing.expect_equal("input", source_text(&tree, node_span(&tree, zero)))
|
||||
try testing.expect_equal("generated.hon", source_files(&tree)[usize(node_span(&tree, zero).source)].path)
|
||||
try testing.expect_equal("generated.hon", source_files(&tree)[usize(node_span(&tree, maximum).source)].path)
|
||||
}
|
||||
|
||||
streams_decoded_string_literal_bytes test {
|
||||
text :: "\"a\\\\\\\"\\n\\r\\t\\0\\x4a\\xFf\""
|
||||
expected [9]u8 :: ['a', '\\', '"', '\n', '\r', '\t', 0, 'J', 255]
|
||||
cursor := usize(1)
|
||||
for (expected) |byte| try testing.expect_equal(byte, try string_next(text, &cursor))
|
||||
try testing.expect_equal(text.len - 1, cursor)
|
||||
malformed [6][]u8 :: ["\"\\q\"", "\"\\x\"", "\"\\x1\"", "\"\\xG0\"", "\"\\x0G\"", "\"\\\""]
|
||||
for (malformed) |literal| {
|
||||
cursor = 1
|
||||
_ = string_next(literal, &cursor) catch |error| {
|
||||
try testing.expect_equal(StringError.invalid_escape, error)
|
||||
continue
|
||||
}
|
||||
try testing.expect(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@std/mem"
|
||||
import "@std/arraylist"
|
||||
import "@std/enummap"
|
||||
import "@source/ast"
|
||||
|
||||
#! owns output bytes and retains its allocator; release with deinit.
|
||||
@@ -26,6 +27,11 @@ render proc(renderer @mut Renderer, tree @ast.Tree) void ! mem.AllocError {
|
||||
match ast.decl_kind(tree, declaration) {
|
||||
.statement: try render_statement(renderer, tree, declaration, " ")
|
||||
.function: try render_function(renderer, tree, declaration)
|
||||
.import: {
|
||||
try append_text(renderer, " ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.import_decl(tree, declaration).span))
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
.invalid: try append_text(renderer, " invalid declaration\n")
|
||||
}
|
||||
}
|
||||
@@ -48,7 +54,7 @@ render_function proc(
|
||||
render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, depth usize) void ! mem.AllocError {
|
||||
for 0..ast.block_statement_count(tree, block) |index| {
|
||||
statement :: ast.block_statement(tree, block, index)
|
||||
for 0..depth |_| try append_text(renderer, " ")
|
||||
for (0..depth) |_| try append_text(renderer, " ")
|
||||
match ast.stmt_kind(tree, statement) {
|
||||
.declaration: try render_statement(renderer, tree, statement, "")
|
||||
.expression: {
|
||||
@@ -59,6 +65,23 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
|
||||
}
|
||||
.break_stmt: try append_text(renderer, "break\n")
|
||||
.continue_stmt: try append_text(renderer, "continue\n")
|
||||
.unreachable_stmt: try append_text(renderer, "unreachable\n")
|
||||
.block_stmt: {
|
||||
try append_text(renderer, "block\n")
|
||||
try render_block(renderer, tree, ast.statement_block(tree, statement), depth + 1)
|
||||
}
|
||||
.defer_stmt, .errdefer_stmt: {
|
||||
cleanup :: ast.cleanup_stmt(tree, statement)
|
||||
keyword :: if (ast.stmt_kind(tree, statement) == .errdefer_stmt) "errdefer" else "defer"
|
||||
try append_text(renderer, keyword)
|
||||
if (cleanup.capture) |capture| {
|
||||
try append_text(renderer, " |")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, capture)))
|
||||
try append_text(renderer, "|")
|
||||
}
|
||||
try append_text(renderer, "\n")
|
||||
try render_block(renderer, tree, cleanup.body, depth + 1)
|
||||
}
|
||||
.return_stmt: {
|
||||
try append_text(renderer, "return")
|
||||
if (ast.return_stmt(tree, statement).value) |value| {
|
||||
@@ -67,6 +90,12 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
|
||||
}
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
.yield_stmt: {
|
||||
try append_text(renderer, "yield ")
|
||||
value :: ast.yield_stmt(tree, statement).value?
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(tree, statement)
|
||||
try append_text(renderer, "if ")
|
||||
@@ -74,7 +103,7 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
|
||||
try append_text(renderer, "\n")
|
||||
try render_block(renderer, tree, conditional.then_block, depth + 1)
|
||||
if (conditional.else_block) |alternative| {
|
||||
for 0..depth |_| try append_text(renderer, " ")
|
||||
for (0..depth) |_| try append_text(renderer, " ")
|
||||
try append_text(renderer, "else\n")
|
||||
try render_block(renderer, tree, alternative, depth + 1)
|
||||
}
|
||||
@@ -83,8 +112,17 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
|
||||
assignment :: ast.assignment_stmt(tree, statement)
|
||||
try append_text(renderer, "assignment ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, assignment.target)))
|
||||
try append_text(renderer, " = ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, assignment.value)))
|
||||
value := assignment.value
|
||||
if (assignment.compound) {
|
||||
binary :: ast.binary_expr(tree, value)
|
||||
try append_text(renderer, " ")
|
||||
try append_text(renderer, enummap.get(&assignment_operator_texts, binary.operator)?)
|
||||
try append_text(renderer, "= ")
|
||||
value = binary.right
|
||||
} else {
|
||||
try append_text(renderer, " = ")
|
||||
}
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
.while_stmt: {
|
||||
@@ -96,22 +134,17 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
|
||||
}
|
||||
.for_stmt: {
|
||||
loop :: ast.for_stmt(tree, statement)
|
||||
if (loop.is_inline) try append_text(renderer, "inline ")
|
||||
try append_text(renderer, "for ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, loop.iterable)))
|
||||
try append_text(renderer, " |")
|
||||
if (loop.pointer_capture) try append_text(renderer, "@")
|
||||
if (loop.capture) |capture| {
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, capture)))
|
||||
} else {
|
||||
try append_text(renderer, "_")
|
||||
}
|
||||
if (loop.capture) |capture| try append_text(renderer, ast.source_text(tree, ast.node_span(tree, capture)))
|
||||
else try append_text(renderer, "_")
|
||||
if (loop.index_capture) {
|
||||
try append_text(renderer, ", ")
|
||||
if (loop.index) |index| {
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, index)))
|
||||
} else {
|
||||
try append_text(renderer, "_")
|
||||
}
|
||||
if (loop.index) |index_capture| try append_text(renderer, ast.source_text(tree, ast.node_span(tree, index_capture)))
|
||||
else try append_text(renderer, "_")
|
||||
}
|
||||
try append_text(renderer, "|\n")
|
||||
try render_block(renderer, tree, loop.body, depth + 1)
|
||||
@@ -136,7 +169,8 @@ render_statement proc(
|
||||
try append_text(renderer, " ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, type)))
|
||||
}
|
||||
try append_text(renderer, if (statement.mutability == .constant) " :: " else " := ")
|
||||
binding :: if (statement.mutability == .constant) " :: " else " := "
|
||||
try append_text(renderer, binding)
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, statement.initializer)))
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
@@ -145,3 +179,20 @@ render_statement proc(
|
||||
append_text proc(renderer @mut Renderer, text []u8) void ! mem.AllocError {
|
||||
for (text) |byte| try arraylist.append(&renderer.bytes, byte)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
assignment_operator_texts :: $enummap.init_default(
|
||||
ast.BinaryOperator, []u8, "", {
|
||||
add = "+",
|
||||
subtract = "-",
|
||||
multiply = "*",
|
||||
divide = "/",
|
||||
bit_and = "&",
|
||||
bit_or = "|",
|
||||
bit_xor = "xor",
|
||||
shift_left = "<<",
|
||||
shift_right = ">>",
|
||||
shift_left_saturating = "<<|",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import "@source/parser"
|
||||
import "@source/ast"
|
||||
|
||||
renders_array_annotations_literals_and_indexed_assignments test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -19,7 +19,7 @@ renders_array_annotations_literals_and_indexed_assignments test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
renderer := init(mem.c_allocator)
|
||||
@@ -31,3 +31,24 @@ renders_array_annotations_literals_and_indexed_assignments test {
|
||||
renderer.bytes.items,
|
||||
)
|
||||
}
|
||||
|
||||
renders_import_declarations test {
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source :: "import \"relative\"\nname :: import \"@root/pkg\"\ntest import \"test_only\"\n"
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
renderer := init(mem.c_allocator)
|
||||
defer deinit(&renderer)
|
||||
try render(&renderer, &tree)
|
||||
try testing.expect_equal(
|
||||
"package\n import \"relative\"\n name :: import \"@root/pkg\"\n test import \"test_only\"\n",
|
||||
renderer.bytes.items,
|
||||
)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import "@std/mem"
|
||||
import "@std/debug"
|
||||
import "@std/arraylist"
|
||||
import "@std/hashmap"
|
||||
import "@std/io"
|
||||
import "@std/fs"
|
||||
import "@source/strpool"
|
||||
import "@source/diagnostics"
|
||||
import "@source/lexer"
|
||||
import "@source/parser"
|
||||
import "@source/ast"
|
||||
import "@source/sema"
|
||||
import "@source/lower"
|
||||
import "@source/loader"
|
||||
import "@source/ir"
|
||||
typecheck :: import "@source/sema/check"
|
||||
import "@source/sema/specialize"
|
||||
|
||||
#! identities are valid only within their owning compilation.
|
||||
SourceId :: alias diagnostics.SourceId
|
||||
PackageId :: distinct usize
|
||||
|
||||
#! node IDs belong to the compilation arena; source identifies their originating file.
|
||||
DeclarationId :: struct {
|
||||
source SourceId
|
||||
node ast.NodeId
|
||||
}
|
||||
|
||||
Source :: struct {
|
||||
package PackageId
|
||||
path []u8
|
||||
bytes arraylist.ArrayList(u8)
|
||||
root ast.NodeId
|
||||
}
|
||||
|
||||
Package :: struct {
|
||||
sources arraylist.ArrayList(SourceId)
|
||||
path []u8 = &[]
|
||||
root ?ast.NodeId = null
|
||||
loading bool = false
|
||||
}
|
||||
|
||||
Import :: struct {
|
||||
node ast.NodeId
|
||||
target ?PackageId
|
||||
}
|
||||
|
||||
#! owns source buffers, paths, the AST arena, package graph and one identifier pool.
|
||||
#! metadata borrows are invalidated by registration; IDs remain stable until deinit.
|
||||
Compilation :: struct {
|
||||
allocator mem.Allocator
|
||||
strings strpool.StringPool
|
||||
builder ast.Builder
|
||||
sources arraylist.ArrayList(Source)
|
||||
packages arraylist.ArrayList(Package)
|
||||
imports arraylist.ArrayList(Import)
|
||||
paths hashmap.StringHashMap(PackageId)
|
||||
module_root ?ast.NodeId = null
|
||||
}
|
||||
|
||||
LoadError :: enum { empty_package }
|
||||
Error :: alias mem.AllocError | diagnostics.CapacityError | lexer.SourceError | strpool.InternError | loader.Error | LoadError
|
||||
|
||||
#! retains `allocator`; release all compilation storage with deinit.
|
||||
init proc(allocator mem.Allocator) Compilation {
|
||||
return Compilation{
|
||||
allocator = allocator,
|
||||
strings = strpool.init(allocator),
|
||||
builder = ast.init_builder(allocator, &[]),
|
||||
sources = arraylist.init(allocator),
|
||||
packages = arraylist.init(allocator),
|
||||
imports = arraylist.init(allocator),
|
||||
paths = hashmap.init(allocator),
|
||||
}
|
||||
}
|
||||
|
||||
deinit proc(compilation @mut Compilation) void {
|
||||
ast.deinit_builder(&compilation.builder)
|
||||
hashmap.deinit(&compilation.paths)
|
||||
for (compilation.sources.items) |@file| {
|
||||
arraylist.deinit(&file.bytes)
|
||||
mem.free(compilation.allocator, file.path)
|
||||
}
|
||||
for (compilation.packages.items) |@package| {
|
||||
arraylist.deinit(&package.sources)
|
||||
mem.free(compilation.allocator, package.path)
|
||||
}
|
||||
arraylist.deinit(&compilation.sources)
|
||||
arraylist.deinit(&compilation.packages)
|
||||
arraylist.deinit(&compilation.imports)
|
||||
strpool.deinit(&compilation.strings)
|
||||
}
|
||||
|
||||
#! creates an in-memory package; load registers filesystem packages by canonical directory.
|
||||
add_package proc(compilation @mut Compilation) PackageId ! mem.AllocError {
|
||||
id :: PackageId(compilation.packages.items.len)
|
||||
try arraylist.append(&compilation.packages, Package{
|
||||
sources = arraylist.init(compilation.allocator),
|
||||
})
|
||||
compilation.module_root = null
|
||||
return id
|
||||
}
|
||||
|
||||
#! copies `path`, scans/parses into the shared arena and takes `bytes` only on success.
|
||||
#! failure leaves `bytes` caller-owned and rolls back source diagnostics; interned names may remain.
|
||||
add_source proc(
|
||||
compilation @mut Compilation,
|
||||
package PackageId,
|
||||
path []u8,
|
||||
bytes @mut arraylist.ArrayList(u8),
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) SourceId ! Error {
|
||||
debug.assert(usize(package) < compilation.packages.items.len)
|
||||
try arraylist.reserve(&compilation.sources, compilation.sources.items.len + 1)
|
||||
members := &compilation.packages.items[usize(package)].sources
|
||||
try arraylist.reserve(members, members.items.len + 1)
|
||||
owned_path :: try mem.alloc(u8, compilation.allocator, path.len)
|
||||
errdefer mem.free(compilation.allocator, owned_path)
|
||||
memcopy!(owned_path, path)
|
||||
id :: try ast.set_source(&compilation.builder, owned_path, bytes.items)
|
||||
errdefer ast.remove_source(&compilation.builder)
|
||||
diagnostic_count :: diagnostics.count(diagnostic_store)
|
||||
errdefer diagnostic_store.list.items = diagnostic_store.list.items[..diagnostic_count]
|
||||
debug.assert(usize(id) == compilation.sources.items.len)
|
||||
scan_state := lexer.init(compilation.allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
scan_state.source_id = id
|
||||
try lexer.scan(&scan_state, bytes.items, &compilation.strings, diagnostic_store)
|
||||
root :: try parser.parse_source(&compilation.builder, scan_state.tokens.items, diagnostic_store)
|
||||
try arraylist.append(&compilation.sources, Source{
|
||||
package = package,
|
||||
path = owned_path,
|
||||
bytes = bytes^,
|
||||
root = root,
|
||||
})
|
||||
try arraylist.append(members, id)
|
||||
bytes^ = arraylist.init(bytes.allocator)
|
||||
compilation.packages.items[usize(package)].root = null
|
||||
compilation.module_root = null
|
||||
return id
|
||||
}
|
||||
|
||||
#! borrows source metadata until registration or deinit.
|
||||
source proc(compilation @Compilation, id SourceId) @Source {
|
||||
debug.assert(usize(id) < compilation.sources.items.len)
|
||||
return &compilation.sources.items[usize(id)]
|
||||
}
|
||||
|
||||
declaration proc(compilation @Compilation, id SourceId, index usize) DeclarationId {
|
||||
return DeclarationId{
|
||||
source = id,
|
||||
node = ast.builder_package_decl(&compilation.builder, source(compilation, id).root, index),
|
||||
}
|
||||
}
|
||||
|
||||
#! borrows source paths and bytes until registration or deinit; valid after loading a source.
|
||||
source_files proc(compilation @Compilation) []diagnostics.Source {
|
||||
tree :: ast.view(&compilation.builder, compilation.sources.items[0].root)
|
||||
return ast.source_files(&tree)
|
||||
}
|
||||
|
||||
#! loads a file alone or every production source in a directory, then its dependency graph.
|
||||
#! directory identities resolve symlinks; file commands do not implicitly include siblings.
|
||||
load proc(
|
||||
compilation @mut Compilation,
|
||||
runtime io.Io,
|
||||
path []u8,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) PackageId ! Error {
|
||||
canonical :: try fs.canonical(compilation.allocator, path)
|
||||
defer mem.free(compilation.allocator, canonical)
|
||||
directory :: try loader.directory(compilation.allocator, canonical)
|
||||
transferred := false
|
||||
defer { if (!transferred) mem.free(compilation.allocator, directory) }
|
||||
project_root :: try loader.project_root(compilation.allocator, directory)
|
||||
defer mem.free(compilation.allocator, project_root)
|
||||
entry_file ?[]u8 := null
|
||||
if (try fs.kind(compilation.allocator, canonical) != .directory) entry_file = canonical
|
||||
transferred = true
|
||||
return try load_package(compilation, runtime, project_root, directory, entry_file, null, diagnostic_store)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
load_package proc(
|
||||
compilation @mut Compilation,
|
||||
runtime io.Io,
|
||||
project_root []u8,
|
||||
owned_directory []mut u8,
|
||||
entry_file ?[]u8,
|
||||
import_span ?diagnostics.Span,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) PackageId ! Error {
|
||||
registered := false
|
||||
defer { if (!registered) mem.free(compilation.allocator, owned_directory) }
|
||||
if (hashmap.get(&compilation.paths, owned_directory)) |existing| {
|
||||
if (compilation.packages.items[usize(existing)].loading) {
|
||||
if (import_span) |span| _ = try diagnostics.add(diagnostic_store, .import_cycle, span)
|
||||
}
|
||||
return existing
|
||||
}
|
||||
id :: try add_package(compilation)
|
||||
compilation.packages.items[usize(id)].path = owned_directory
|
||||
compilation.packages.items[usize(id)].loading = true
|
||||
registered = true
|
||||
hashmap.put(&compilation.paths, owned_directory, id) catch |err| {
|
||||
match err {
|
||||
.key_exists: unreachable
|
||||
else: return err
|
||||
}
|
||||
}
|
||||
read_failed := false
|
||||
load_sources(compilation, runtime, id, entry_file, diagnostic_store) catch |err| {
|
||||
match err {
|
||||
.out_of_memory, .diagnostic_limit_reached, .storage_limit_reached, .out_of_space: return err
|
||||
else: {
|
||||
if (import_span) |span| {
|
||||
_ = try diagnostics.add(diagnostic_store, .package_unavailable, span)
|
||||
read_failed = true
|
||||
} else return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if (compilation.packages.items[usize(id)].sources.items.len == 0) {
|
||||
if (import_span) |span| {
|
||||
if (!read_failed) _ = try diagnostics.add(diagnostic_store, .package_unavailable, span)
|
||||
} else return .empty_package
|
||||
}
|
||||
# Source and package arrays may grow during recursion; retain IDs, never their element addresses.
|
||||
for (0..(compilation.packages.items[usize(id)].sources.items.len)) |index| {
|
||||
file_id :: compilation.packages.items[usize(id)].sources.items[index]
|
||||
root :: source(compilation, file_id).root
|
||||
count :: ast.builder_package_decl_count(&compilation.builder, root)
|
||||
for (0..count) |declaration_index| {
|
||||
node :: ast.builder_package_decl(&compilation.builder, root, declaration_index)
|
||||
tree :: ast.view(&compilation.builder, root)
|
||||
if (ast.decl_kind(&tree, node) != .import) continue
|
||||
imported :: ast.import_decl(&tree, node)
|
||||
if (imported.test_only) continue
|
||||
path :: ast.source_text(&tree, imported.path)
|
||||
target ?PackageId := null
|
||||
if (path.len == 0 or path[0] == '/' or (path.len == 1 and path[0] == '@')) {
|
||||
_ = try diagnostics.add(diagnostic_store, .invalid_import_path, imported.path)
|
||||
} else {
|
||||
resolved := loader.resolve(
|
||||
compilation.allocator,
|
||||
project_root,
|
||||
compilation.packages.items[usize(id)].path,
|
||||
path,
|
||||
) catch |err| {
|
||||
match err {
|
||||
.out_of_memory: return err
|
||||
.invalid_path: {
|
||||
_ = try diagnostics.add(diagnostic_store, .invalid_import_path, imported.path)
|
||||
try arraylist.append(&compilation.imports, Import{ node = node, target = null })
|
||||
continue
|
||||
}
|
||||
else: {
|
||||
_ = try diagnostics.add(diagnostic_store, .package_unavailable, imported.path)
|
||||
try arraylist.append(&compilation.imports, Import{ node = node, target = null })
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
owned := true
|
||||
defer { if (owned) mem.free(compilation.allocator, resolved) }
|
||||
if (try fs.kind(compilation.allocator, resolved) == .directory) {
|
||||
owned = false
|
||||
target = try load_package(compilation, runtime, project_root, resolved, null, imported.path, diagnostic_store)
|
||||
} else {
|
||||
_ = try diagnostics.add(diagnostic_store, .package_unavailable, imported.path)
|
||||
}
|
||||
}
|
||||
try arraylist.append(&compilation.imports, Import{ node = node, target = target })
|
||||
}
|
||||
}
|
||||
compilation.packages.items[usize(id)].loading = false
|
||||
return id
|
||||
}
|
||||
|
||||
@hide:file
|
||||
load_sources proc(
|
||||
compilation @mut Compilation,
|
||||
runtime io.Io,
|
||||
package PackageId,
|
||||
entry_file ?[]u8,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) void ! Error {
|
||||
if (entry_file) |path| {
|
||||
try load_source(compilation, runtime, package, path, diagnostic_store)
|
||||
return
|
||||
}
|
||||
files := try loader.files(compilation.allocator, compilation.packages.items[usize(package)].path)
|
||||
defer {
|
||||
for (files.items) |path| mem.free(compilation.allocator, path)
|
||||
arraylist.deinit(&files)
|
||||
}
|
||||
for (files.items) |path| try load_source(compilation, runtime, package, path, diagnostic_store)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
load_source proc(
|
||||
compilation @mut Compilation,
|
||||
runtime io.Io,
|
||||
package PackageId,
|
||||
path []u8,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) void ! Error {
|
||||
bytes := try loader.read_source(compilation.allocator, runtime, path)
|
||||
defer arraylist.deinit(&bytes)
|
||||
_ = try add_source(compilation, package, path, &bytes, diagnostic_store)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
package_root proc(compilation @mut Compilation, package PackageId) ast.NodeId ! mem.AllocError {
|
||||
if (compilation.packages.items[usize(package)].root) |root| return root
|
||||
declarations := arraylist.init(ast.NodeId, compilation.allocator)
|
||||
defer arraylist.deinit(&declarations)
|
||||
span := diagnostics.Span{ start = 0, end = 0 }
|
||||
for (compilation.packages.items[usize(package)].sources.items) |id, index| {
|
||||
root :: source(compilation, id).root
|
||||
tree :: ast.view(&compilation.builder, root)
|
||||
if (index == 0) span = ast.node_span(&tree, root)
|
||||
for (0..ast.package_decl_count(&tree)) |i| try arraylist.append(&declarations, ast.package_decl(&tree, i))
|
||||
}
|
||||
root :: try ast.add_package(&compilation.builder, span, declarations.items)
|
||||
compilation.packages.items[usize(package)].root = root
|
||||
return root
|
||||
}
|
||||
|
||||
#! checks every package in a separate scope and lowers one module with root-only entry selection.
|
||||
#! returned IR borrows no frontend data; release it with ir.deinit.
|
||||
#! temporary compilation storage uses the retained allocator and is released before returning.
|
||||
compile proc(
|
||||
compilation @mut Compilation,
|
||||
root_package PackageId,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) ir.Module ! Error {
|
||||
roots := arraylist.init(ast.NodeId, compilation.allocator)
|
||||
defer arraylist.deinit(&roots)
|
||||
declarations := arraylist.init(ast.NodeId, compilation.allocator)
|
||||
defer arraylist.deinit(&declarations)
|
||||
for (0..(compilation.packages.items.len)) |index| {
|
||||
root :: try package_root(compilation, PackageId(index))
|
||||
try arraylist.append(&roots, root)
|
||||
if (compilation.module_root == null) {
|
||||
tree :: ast.view(&compilation.builder, root)
|
||||
for (0..ast.package_decl_count(&tree)) |i| try arraylist.append(&declarations, ast.package_decl(&tree, i))
|
||||
}
|
||||
}
|
||||
if (compilation.module_root == null) compilation.module_root = try ast.add_package(&compilation.builder, diagnostics.Span{ start = 0, end = 0 }, declarations.items)
|
||||
imports := arraylist.init(sema.Import, compilation.allocator)
|
||||
defer arraylist.deinit(&imports)
|
||||
tree := ast.view(&compilation.builder, compilation.module_root?)
|
||||
for (compilation.imports.items) |link| {
|
||||
imported :: ast.import_decl(&tree, link.node)
|
||||
name := strpool.no_str
|
||||
failure ?diagnostics.DiagnosticId := null
|
||||
for (diagnostic_store.list.items) |diagnostic, index| {
|
||||
if (
|
||||
diagnostic.span.source == imported.path.source
|
||||
and diagnostic.span.start == imported.path.start
|
||||
and diagnostic.span.end == imported.path.end
|
||||
and (diagnostic.code == .package_unavailable
|
||||
or diagnostic.code == .invalid_import_path
|
||||
or diagnostic.code == .import_cycle)
|
||||
) {
|
||||
failure = diagnostics.DiagnosticId(index)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (imported.namespace) |explicit| {
|
||||
name = explicit
|
||||
} else {
|
||||
path :: if (link.target) |target|
|
||||
compilation.packages.items[usize(target)].path
|
||||
else
|
||||
ast.source_text(&tree, imported.path)
|
||||
start := path.len
|
||||
while (start > 0 and path[start - 1] != '/') start -= 1
|
||||
candidate :: path[start..]
|
||||
valid := candidate.len > 0
|
||||
for (candidate) |byte, index| {
|
||||
if (
|
||||
!(byte >= 'a' and byte <= 'z')
|
||||
and !(byte >= 'A' and byte <= 'Z')
|
||||
and byte != '_'
|
||||
and !(index > 0 and byte >= '0' and byte <= '9')
|
||||
) valid = false
|
||||
}
|
||||
if (valid) {
|
||||
name = try strpool.intern(&compilation.strings, candidate)
|
||||
} else if (failure == null) {
|
||||
failure = try diagnostics.add(diagnostic_store, .invalid_import_alias, imported.path)
|
||||
}
|
||||
}
|
||||
target ?ast.NodeId :: if (link.target) |package| roots.items[usize(package)] else null
|
||||
try arraylist.append(&imports, sema.Import{ node = link.node, name = name, target = target, diagnostic = failure })
|
||||
}
|
||||
resolution := try sema.resolve_packages(compilation.allocator, &tree, roots.items, imports.items, &compilation.strings, diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
elaborated := try specialize.elaborate(
|
||||
compilation.allocator,
|
||||
&compilation.builder,
|
||||
compilation.module_root?,
|
||||
&resolution,
|
||||
&compilation.strings,
|
||||
diagnostic_store,
|
||||
)
|
||||
defer specialize.deinit(&elaborated)
|
||||
tree = ast.view(&compilation.builder, compilation.module_root?)
|
||||
checked := try typecheck.check_elaborated(compilation.allocator, &tree, &resolution, elaborated.constraints.items, diagnostic_store)
|
||||
defer typecheck.deinit(&checked)
|
||||
return try lower.lower_package(compilation.allocator, &tree, &resolution, &checked, roots.items[usize(root_package)], diagnostic_store)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,63 +2,142 @@ import "@std/mem"
|
||||
import "@std/arraylist"
|
||||
import "@std/enummap"
|
||||
|
||||
SourceId :: distinct usize
|
||||
|
||||
#! borrows a source path and text owned by the compilation or caller.
|
||||
Source :: struct {
|
||||
path []u8
|
||||
text []u8
|
||||
}
|
||||
|
||||
Span :: struct {
|
||||
start u32
|
||||
end u32
|
||||
source SourceId = SourceId(0)
|
||||
}
|
||||
|
||||
DiagnosticId :: distinct u32
|
||||
no_diagnostic :: maxval!(DiagnosticId)
|
||||
|
||||
Code :: enum {
|
||||
invalid_character,
|
||||
float_must_end_with_digit,
|
||||
unterminated_string,
|
||||
unexpected_token,
|
||||
duplicate_name,
|
||||
unknown_name,
|
||||
expected_type,
|
||||
type_not_value,
|
||||
invalid_void_type,
|
||||
type_mismatch,
|
||||
invalid_operand,
|
||||
integer_division,
|
||||
not_callable,
|
||||
argument_count_mismatch,
|
||||
cannot_infer_type,
|
||||
missing_return,
|
||||
return_type_mismatch,
|
||||
condition_not_bool,
|
||||
invalid_assignment_target,
|
||||
break_outside_loop,
|
||||
continue_outside_loop,
|
||||
invalid_range_bound,
|
||||
invalid_for_iterable,
|
||||
range_value_not_supported,
|
||||
range_pointer_capture,
|
||||
range_index_capture,
|
||||
invalid_array_length,
|
||||
array_length_mismatch,
|
||||
invalid_array_element_type,
|
||||
array_size_overflow,
|
||||
invalid_index_type,
|
||||
not_indexable,
|
||||
invalid_index_assignment,
|
||||
index_out_of_bounds,
|
||||
missing_entry,
|
||||
invalid_entry,
|
||||
literal_out_of_range,
|
||||
inexact_integer_literal,
|
||||
invalid_escape,
|
||||
read_before_initialization,
|
||||
write_before_initialization,
|
||||
initialization_cycle,
|
||||
integer_overflow,
|
||||
invalid_character
|
||||
float_must_end_with_digit
|
||||
unterminated_string
|
||||
unterminated_character
|
||||
invalid_character_literal
|
||||
unexpected_token
|
||||
duplicate_name
|
||||
unknown_name
|
||||
expected_type
|
||||
type_not_value
|
||||
invalid_void_type
|
||||
type_mismatch
|
||||
invalid_operand
|
||||
integer_division
|
||||
not_callable
|
||||
argument_count_mismatch
|
||||
cannot_infer_type
|
||||
missing_return
|
||||
return_type_mismatch
|
||||
yield_outside_block
|
||||
missing_yield
|
||||
yield_type_mismatch
|
||||
condition_not_bool
|
||||
invalid_assignment_target
|
||||
break_outside_loop
|
||||
continue_outside_loop
|
||||
invalid_range_bound
|
||||
invalid_for_iterable
|
||||
range_value_not_supported
|
||||
range_pointer_capture
|
||||
range_index_capture
|
||||
invalid_array_length
|
||||
array_length_mismatch
|
||||
invalid_array_element_type
|
||||
array_size_overflow
|
||||
invalid_index_type
|
||||
not_indexable
|
||||
invalid_index_assignment
|
||||
index_out_of_bounds
|
||||
unknown_field
|
||||
duplicate_field
|
||||
missing_field
|
||||
tuple_arity_mismatch
|
||||
not_record
|
||||
recursive_record
|
||||
invalid_record_field_type
|
||||
missing_entry
|
||||
invalid_entry
|
||||
literal_out_of_range
|
||||
inexact_integer_literal
|
||||
invalid_escape
|
||||
read_before_initialization
|
||||
write_before_initialization
|
||||
initialization_cycle
|
||||
integer_overflow
|
||||
division_by_zero
|
||||
import_cycle
|
||||
invalid_import_path
|
||||
package_unavailable
|
||||
namespace_not_value
|
||||
not_namespace
|
||||
invalid_import_alias
|
||||
invalid_enum_backing
|
||||
invalid_enum_value
|
||||
enum_context_required
|
||||
invalid_distinct_backing
|
||||
invalid_union_tag
|
||||
union_initializer_count
|
||||
invalid_match_subject
|
||||
invalid_match_pattern
|
||||
duplicate_match_pattern
|
||||
non_exhaustive_match
|
||||
unreachable_match_arm
|
||||
invalid_match_capture
|
||||
invalid_inline_match_subject
|
||||
invalid_inline_match_capture
|
||||
inactive_union_field
|
||||
invalid_pointer_type
|
||||
invalid_address_operand
|
||||
invalid_dereference
|
||||
invalid_pointer_assignment
|
||||
invalid_pointer_access
|
||||
null_unwrap
|
||||
invalid_slice_bounds
|
||||
sentinel_mismatch
|
||||
overlapping_memory_copy
|
||||
unsupported_foreign_call
|
||||
invalid_fallible_type
|
||||
invalid_sum_type
|
||||
invalid_fallible_operand
|
||||
try_outside_fallible
|
||||
incompatible_propagated_error
|
||||
catch_type_mismatch
|
||||
errdefer_outside_fallible
|
||||
cleanup_control_flow
|
||||
comptime_argument_required
|
||||
cannot_infer_generic
|
||||
specialization_cycle
|
||||
specialization_limit
|
||||
comptime_execution_limit
|
||||
comptime_value_escape
|
||||
inline_loop_control
|
||||
invalid_generated_field_name
|
||||
invalid_struct_layout
|
||||
invalid_c_record_field_type
|
||||
missing_metadata_import
|
||||
invalid_metadata_abi
|
||||
invalid_undefined
|
||||
invalid_shift_count
|
||||
unreachable_executed
|
||||
compile_error
|
||||
invalid_compile_error
|
||||
}
|
||||
|
||||
Diagnostic :: struct {
|
||||
code Code
|
||||
span Span
|
||||
custom_message ?[]u8 = null
|
||||
}
|
||||
|
||||
#! owns diagnostic storage and retains its allocator; release with deinit.
|
||||
@@ -73,6 +152,9 @@ init proc(allocator mem.Allocator) Store {
|
||||
|
||||
#! releases diagnostic storage with the retained allocator.
|
||||
deinit proc(store @mut Store) void {
|
||||
for (store.list.items) |diagnostic| {
|
||||
if (diagnostic.custom_message) |text| mem.free(store.list.allocator, text)
|
||||
}
|
||||
arraylist.deinit(&store.list)
|
||||
}
|
||||
|
||||
@@ -84,6 +166,22 @@ add proc(store @mut Store, code Code, span Span) DiagnosticId ! (mem.AllocError
|
||||
return id
|
||||
}
|
||||
|
||||
#! copies `text`; the store owns the message until deinit.
|
||||
add_message proc(store @mut Store, code Code, span Span, text []u8) DiagnosticId ! (mem.AllocError | CapacityError) {
|
||||
owned :: try mem.alloc(u8, store.list.allocator, text.len)
|
||||
errdefer mem.free(store.list.allocator, owned)
|
||||
memcopy!(owned, text)
|
||||
id :: try add(store, code, span)
|
||||
store.list.items[usize(id)].custom_message = owned
|
||||
return id
|
||||
}
|
||||
|
||||
#! borrows either the store-owned custom message or process-lifetime code text.
|
||||
message proc(diagnostic Diagnostic) []u8 {
|
||||
if (diagnostic.custom_message) |text| return text
|
||||
return enummap.get(Code, []u8, &messages, diagnostic.code)?
|
||||
}
|
||||
|
||||
get proc(store @Store, id DiagnosticId) ?Diagnostic {
|
||||
if (usize(id) >= store.list.items.len) return null
|
||||
return store.list.items[usize(id)]
|
||||
@@ -98,6 +196,8 @@ messages :: $enummap.init(Code, []u8, {
|
||||
invalid_character = "invalid character",
|
||||
float_must_end_with_digit = "float literal must end with a digit",
|
||||
unterminated_string = "unterminated string literal",
|
||||
unterminated_character = "unterminated character literal",
|
||||
invalid_character_literal = "character literal must contain one Unicode code point",
|
||||
unexpected_token = "unexpected token",
|
||||
duplicate_name = "duplicate name or forbidden shadowing",
|
||||
unknown_name = "unknown name",
|
||||
@@ -107,17 +207,22 @@ messages :: $enummap.init(Code, []u8, {
|
||||
type_mismatch = "incompatible value type",
|
||||
invalid_operand = "operator requires compatible operand types",
|
||||
integer_division = "division requires float operands",
|
||||
invalid_shift_count = "shift count must be smaller than the integer bit width",
|
||||
not_callable = "value is not callable",
|
||||
argument_count_mismatch = "argument count does not match the function signature",
|
||||
cannot_infer_type = "cannot infer a type without a type anchor",
|
||||
missing_return = "nonvoid function can finish without returning a value",
|
||||
unreachable_executed = "reached unreachable statement",
|
||||
return_type_mismatch = "return does not match the function result type",
|
||||
yield_outside_block = "yield requires an enclosing value block",
|
||||
missing_yield = "nonvoid value block can finish without yielding a value",
|
||||
yield_type_mismatch = "yield does not match the value block result type",
|
||||
condition_not_bool = "condition must have type bool",
|
||||
invalid_assignment_target = "assignment requires a mutable local binding",
|
||||
break_outside_loop = "break requires an enclosing loop",
|
||||
continue_outside_loop = "continue requires an enclosing loop",
|
||||
invalid_range_bound = "range bounds must have compatible integer types",
|
||||
invalid_for_iterable = "for-loop iteration currently requires a direct integer range",
|
||||
invalid_for_iterable = "for-loop iteration requires an array, slice, string, single pointer to an array, direct integer range, or inline tuple",
|
||||
range_value_not_supported = "first-class range values are not implemented",
|
||||
range_pointer_capture = "range loops do not support pointer captures",
|
||||
range_index_capture = "range loops do not support index captures",
|
||||
@@ -129,8 +234,15 @@ messages :: $enummap.init(Code, []u8, {
|
||||
not_indexable = "indexing is currently implemented for fixed arrays only",
|
||||
invalid_index_assignment = "indexed assignment requires a writable array element",
|
||||
index_out_of_bounds = "index out of bounds",
|
||||
unknown_field = "record has no such field",
|
||||
duplicate_field = "record field is specified more than once",
|
||||
missing_field = "record initializer is missing a required field",
|
||||
tuple_arity_mismatch = "tuple initializer arity does not match its type",
|
||||
not_record = "field selection requires a record or tuple",
|
||||
recursive_record = "record has a recursive by-value layout",
|
||||
invalid_record_field_type = "record fields require a concrete value type",
|
||||
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",
|
||||
inexact_integer_literal = "integer literal is not exactly representable as float; use a floating literal to allow rounding",
|
||||
invalid_escape = "invalid string escape",
|
||||
@@ -138,5 +250,60 @@ messages :: $enummap.init(Code, []u8, {
|
||||
write_before_initialization = "local value was assigned before initialization",
|
||||
initialization_cycle = "global initialization cycle",
|
||||
integer_overflow = "integer arithmetic overflow",
|
||||
division_by_zero = "integer division by zero",
|
||||
import_cycle = "package import cycle",
|
||||
invalid_import_path = "invalid import path",
|
||||
package_unavailable = "cannot load package directory",
|
||||
namespace_not_value = "package namespace cannot be used as a value",
|
||||
not_namespace = "member selection requires a package namespace",
|
||||
invalid_import_alias = "import requires an explicit valid identifier alias",
|
||||
invalid_enum_backing = "enum backing must be a concrete integer type",
|
||||
invalid_enum_value = "enum values require explicit backing and increasing representable constant integers",
|
||||
enum_context_required = "enum literal requires an enum or tagged union context",
|
||||
invalid_distinct_backing = "distinct requires a concrete runtime backing type",
|
||||
invalid_union_tag = "union tag must be an enum containing every union variant",
|
||||
union_initializer_count = "union construction requires exactly one member",
|
||||
invalid_match_subject = "match subject must be an enum, tagged union, or scalar",
|
||||
invalid_match_pattern = "invalid match pattern",
|
||||
duplicate_match_pattern = "duplicate or overlapping match pattern",
|
||||
non_exhaustive_match = "match is not exhaustive",
|
||||
unreachable_match_arm = "match arm is unreachable",
|
||||
invalid_match_capture = "payload capture requires compatible nonvoid tagged union variants",
|
||||
invalid_inline_match_subject = "inline match arm requires an enum or tagged union",
|
||||
invalid_inline_match_capture = "inline match arm requires one enum capture or one payload capture and an optional tag",
|
||||
inactive_union_field = "tagged union field is not active",
|
||||
invalid_pointer_type = "pointer requires a supported concrete pointee type",
|
||||
invalid_address_operand = "address-of requires addressable storage",
|
||||
invalid_dereference = "dereference requires a pointer",
|
||||
invalid_pointer_assignment = "pointer assignment requires a mutable pointee",
|
||||
invalid_pointer_access = "pointer does not refer to live accessible storage",
|
||||
null_unwrap = "cannot unwrap a null optional",
|
||||
invalid_slice_bounds = "slice bounds are invalid",
|
||||
sentinel_mismatch = "slice sentinel does not match the expected value",
|
||||
overlapping_memory_copy = "memory copy source and destination overlap",
|
||||
unsupported_foreign_call = "foreign function calls are unsupported by the interpreter",
|
||||
invalid_fallible_type = "fallible type requires a concrete success type and an enum, struct, or tagged union error channel",
|
||||
invalid_sum_type = "sum composition requires compatible named enum, struct, or tagged union variants",
|
||||
invalid_fallible_operand = "try and catch require a fallible operand",
|
||||
try_outside_fallible = "try requires an enclosing fallible function",
|
||||
incompatible_propagated_error = "propagated errors are not a subset of the function error channel",
|
||||
catch_type_mismatch = "catch handler must produce the success type or terminate",
|
||||
errdefer_outside_fallible = "errdefer requires an enclosing fallible function",
|
||||
cleanup_control_flow = "cleanup cannot transfer control out of itself",
|
||||
comptime_argument_required = "generic argument must be a compile-time value",
|
||||
cannot_infer_generic = "cannot infer consistent compile-time arguments from arguments and result context",
|
||||
specialization_cycle = "compile-time specialization depends on its unfinished result",
|
||||
specialization_limit = "compile-time specialization exceeds its recursion or instance limit",
|
||||
comptime_execution_limit = "compile-time execution exceeds its recursion or step limit",
|
||||
comptime_value_escape = "compile-time result cannot escape evaluation as a source value",
|
||||
inline_loop_control = "break or continue targeting an inline loop must be compile-time-resolvable",
|
||||
invalid_generated_field_name = "generated field name must be an ASCII identifier",
|
||||
invalid_struct_layout = "generated record layout must be .auto or .c; C-layout records require at least one field",
|
||||
invalid_c_record_field_type = "record field is not a supported C-layout value type",
|
||||
missing_metadata_import = "typeinfo! requires importing @std/meta",
|
||||
invalid_metadata_abi = "@std/meta declarations do not match the compiler reflection ABI",
|
||||
invalid_undefined = "undefined requires a concrete mutable local declaration",
|
||||
compile_error = "explicit compile-time error",
|
||||
invalid_compile_error = "compile_error! requires one compile-time string argument",
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,10 @@ add_function proc(
|
||||
locals []ir.Type,
|
||||
parameter_count usize,
|
||||
result ir.Type,
|
||||
value_count usize,
|
||||
) ir.FunctionId ! mem.AllocError {
|
||||
) ir.FunctionId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
function := ir.init_function(module.allocator)
|
||||
errdefer {
|
||||
transferred := false
|
||||
errdefer if (!transferred) {
|
||||
arraylist.deinit(&function.parameters)
|
||||
arraylist.deinit(&function.locals)
|
||||
arraylist.deinit(&function.array_storage)
|
||||
@@ -24,22 +24,24 @@ add_function proc(
|
||||
}
|
||||
arraylist.deinit(&function.blocks)
|
||||
}
|
||||
for (locals) |type, index| {
|
||||
try arraylist.append(&function.locals, type)
|
||||
if (index < parameter_count) try arraylist.append(&function.parameters, type)
|
||||
}
|
||||
function.result = result
|
||||
function.value_count = value_count
|
||||
try arraylist.append(&function.blocks, ir.init_block(module.allocator))
|
||||
id :: ir.FunctionId(module.functions.items.len)
|
||||
try arraylist.append(&module.functions, function)
|
||||
transferred = true
|
||||
for (locals) |type, index| {
|
||||
if (index < parameter_count) {
|
||||
_ = try ir.add_parameter(module, id, type, ir.no_array)
|
||||
} else {
|
||||
_ = try ir.add_local(module, id, type, ir.no_array)
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError {
|
||||
if (instruction.type == .address) module.functions.items[usize(function)].has_addresses = true
|
||||
try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction)
|
||||
emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
_ = try ir.emit(module, function, ir.BlockId(0), instruction)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
@@ -91,15 +93,15 @@ repeated_calls_have_fresh_parameters_locals_and_temporaries test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [2]ir.Type :: [.int, .int]
|
||||
bump :: try add_function(&module, types[..], 1, .int, 4)
|
||||
bump :: try add_function(&module, types[..], 1, .int)
|
||||
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
try emit(&module, bump, integer(1, 1))
|
||||
try emit(&module, bump, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||
try emit(&module, bump, ir.Instruction{ op = .store_local, left = ir.ValueId(2), slot = 1 })
|
||||
try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(3), slot = 1 })
|
||||
return_value(&module, bump, ir.ValueId(3))
|
||||
noop :: try add_function(&module, mem.empty(ir.Type), 0, .void, 0)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 8)
|
||||
noop :: try add_function(&module, &[], 0, .void)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, function_value(0, bump))
|
||||
try emit(&module, main, integer(1, 19))
|
||||
@@ -125,8 +127,8 @@ lazy_globals_follow_forward_dependencies_and_reset_between_runs test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||
first :: try add_function(&module, &[], 0, .int)
|
||||
second :: try add_function(&module, &[], 0, .int)
|
||||
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
|
||||
try emit(&module, first, integer(1, 1))
|
||||
try emit(&module, first, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||
@@ -135,7 +137,7 @@ lazy_globals_follow_forward_dependencies_and_reset_between_runs test {
|
||||
return_value(&module, second, ir.ValueId(0))
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(1), slot = 0 })
|
||||
@@ -164,7 +166,7 @@ function_aliases_and_borrowed_string_values_execute test {
|
||||
memcopy!(owned, text)
|
||||
try arraylist.append(&module.strings, ir.StringStorage{ allocation = owned, len = text.len })
|
||||
transferred = true
|
||||
target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1)
|
||||
target :: try add_function(&module, &[], 0, .string)
|
||||
try emit(&module, target, ir.Instruction{
|
||||
op = .constant,
|
||||
type = .string,
|
||||
@@ -172,11 +174,11 @@ function_aliases_and_borrowed_string_values_execute test {
|
||||
constant = ir.Value{ type = .string, data = ir.ValueData{ string = ir.StringId(0) } },
|
||||
})
|
||||
return_value(&module, target, ir.ValueId(0))
|
||||
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1)
|
||||
initializer :: try add_function(&module, &[], 0, .function)
|
||||
try emit(&module, initializer, function_value(0, target))
|
||||
return_value(&module, initializer, ir.ValueId(0))
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .function, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .string, 2)
|
||||
main :: try add_function(&module, &[], 0, .string)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .load_global, type = .function, destination = ir.ValueId(0), slot = 0 })
|
||||
try emit(&module, main, ir.Instruction{ op = .call, type = .string, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
@@ -195,7 +197,7 @@ premature_local_reads_and_global_cycles_report_instruction_spans test {
|
||||
defer diagnostics.deinit(&store)
|
||||
span :: diagnostics.Span{ start = 14, end = 19 }
|
||||
types [1]ir.Type :: [.int]
|
||||
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span })
|
||||
return_value(&module, main, ir.ValueId(0))
|
||||
@@ -203,8 +205,8 @@ premature_local_reads_and_global_cycles_report_instruction_spans test {
|
||||
defer deinit(&premature)
|
||||
try expect_trap(premature, &store, .read_before_initialization, span)
|
||||
|
||||
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||
second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||
first :: try add_function(&module, &[], 0, .int)
|
||||
second :: try add_function(&module, &[], 0, .int)
|
||||
cycle_span :: diagnostics.Span{ start = 40, end = 44 }
|
||||
try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 })
|
||||
try emit(&module, second, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0, span = cycle_span })
|
||||
@@ -212,7 +214,10 @@ premature_local_reads_and_global_cycles_report_instruction_spans test {
|
||||
return_value(&module, second, ir.ValueId(0))
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }
|
||||
cycle_main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = cycle_main
|
||||
try emit(&module, cycle_main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
return_value(&module, cycle_main, ir.ValueId(0))
|
||||
cycle := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&cycle)
|
||||
try expect_trap(cycle, &store, .initialization_cycle, cycle_span)
|
||||
@@ -232,9 +237,9 @@ existing_trap_identity_entry_failures_and_unused_errors_survive test {
|
||||
defer deinit(&absent)
|
||||
try testing.expect_equal(entry_id, absent.trap?)
|
||||
invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 })
|
||||
bad :: try add_function(&module, mem.empty(ir.Type), 0, .invalid, 0)
|
||||
bad :: try add_function(&module, &[], 0, .invalid)
|
||||
module.functions.items[usize(bad)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = invalid_id }
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, 42))
|
||||
return_value(&module, main, ir.ValueId(0))
|
||||
@@ -242,8 +247,11 @@ existing_trap_identity_entry_failures_and_unused_errors_survive test {
|
||||
defer deinit(&success)
|
||||
try testing.expect(success.trap == null)
|
||||
try testing.expect_equal(i64(42), success.value.data.integer)
|
||||
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = function_value(0, bad)
|
||||
try emit(&module, main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
trap_main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = trap_main
|
||||
try emit(&module, trap_main, function_value(0, bad))
|
||||
try emit(&module, trap_main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
return_value(&module, trap_main, ir.ValueId(0))
|
||||
trapped := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&trapped)
|
||||
try testing.expect_equal(invalid_id, trapped.trap?)
|
||||
@@ -286,7 +294,7 @@ integer_arithmetic_checks_boundaries_before_host_operations test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
span :: diagnostics.Span{ start = 8, end = 16 }
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, case.left))
|
||||
try emit(&module, main, integer(1, case.right))
|
||||
@@ -303,13 +311,43 @@ integer_arithmetic_checks_boundaries_before_host_operations test {
|
||||
}
|
||||
}
|
||||
|
||||
integer_remainder_preserves_dividend_sign_and_minimum_without_overflow test {
|
||||
cases [6]IntegerCase :: [
|
||||
IntegerCase{ op = .remainder, left = -5, right = 3, expected = -2 },
|
||||
IntegerCase{ op = .remainder, left = 5, right = -3, expected = 2 },
|
||||
IntegerCase{ op = .remainder, left = minval!(i64), right = -1, expected = 0 },
|
||||
IntegerCase{ op = .remainder, left = minval!(i64), right = 10, expected = -8 },
|
||||
IntegerCase{ op = .remainder, left = 0, right = -1, expected = 0 },
|
||||
IntegerCase{ op = .remainder, left = 1, right = 0, expected = null },
|
||||
]
|
||||
for (cases) |case| {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
span :: diagnostics.Span{ start = 8, end = 16 }
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, case.left))
|
||||
try emit(&module, main, integer(1, case.right))
|
||||
try emit(&module, main, ir.Instruction{ op = .remainder, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span })
|
||||
return_value(&module, main, ir.ValueId(2))
|
||||
outcome := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&outcome)
|
||||
if case.expected |expected| {
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(expected, outcome.value.data.integer)
|
||||
} else try expect_trap(outcome, &store, .division_by_zero, span)
|
||||
}
|
||||
}
|
||||
|
||||
integer_negation_traps_only_at_minimum test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
span :: diagnostics.Span{ start = 21, end = 27 }
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, minval!(i64)))
|
||||
try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span })
|
||||
@@ -329,7 +367,7 @@ float_arithmetic_and_ieee_zero_division_execute test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .float, 7)
|
||||
main :: try add_function(&module, &[], 0, .float)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 10.5 } } })
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(1), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 2.0 } } })
|
||||
@@ -413,7 +451,7 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
|
||||
main :: try add_function(&module, &[], 0, .void)
|
||||
module.entry = main
|
||||
recursive_block :: try add_block(&module, main)
|
||||
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = recursive_block }
|
||||
@@ -440,13 +478,13 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
initializer :: try add_function(&module, types[..], 0, .int, 1)
|
||||
initializer :: try add_function(&module, types[..], 0, .int)
|
||||
init_block :: try add_block(&module, initializer)
|
||||
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = init_block }
|
||||
try emit_at(&module, initializer, init_block, integer(0, 42))
|
||||
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ value = ir.ValueId(0) }
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
work :: try add_block(&module, main)
|
||||
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = work }
|
||||
@@ -486,11 +524,72 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
}
|
||||
|
||||
evaluation_backing_survives_returns_and_releases_at_every_allocation_boundary test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.u8]
|
||||
helper :: try add_function(&module, types[..], 0, .pointer)
|
||||
try emit(&module, helper, ir.Instruction{
|
||||
op = .constant, type = .u8, destination = ir.ValueId(0),
|
||||
constant = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 42 } },
|
||||
})
|
||||
try emit(&module, helper, ir.Instruction{ op = .store_local, left = ir.ValueId(0) })
|
||||
try emit(&module, helper, ir.Instruction{
|
||||
op = .address_local, type = .address, pointee = .u8, destination = ir.ValueId(1),
|
||||
})
|
||||
try emit(&module, helper, ir.Instruction{
|
||||
op = .pointer_from_address, type = .pointer, left = ir.ValueId(1), destination = ir.ValueId(2),
|
||||
})
|
||||
return_value(&module, helper, ir.ValueId(2))
|
||||
main :: try add_function(&module, &[], 0, .pointer)
|
||||
module.entry = main
|
||||
try emit(&module, main, function_value(0, helper))
|
||||
try emit(&module, main, ir.Instruction{
|
||||
op = .call, type = .pointer, left = ir.ValueId(0), destination = ir.ValueId(1),
|
||||
})
|
||||
return_value(&module, main, ir.ValueId(1))
|
||||
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
|
||||
for [false, true] |trap| {
|
||||
if (trap) module.functions.items[usize(main)].blocks.items[0].terminator =
|
||||
ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
succeeded := false
|
||||
for (usize(0)..usize(64)) |limit| {
|
||||
state := AllocationState{ remaining = limit }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
outcome := run_evaluation(allocator, &module, &store) catch |error| {
|
||||
match error {
|
||||
.out_of_memory: {}
|
||||
else: return .expectation_failed
|
||||
}
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
continue
|
||||
}
|
||||
defer deinit(&outcome)
|
||||
if (trap) {
|
||||
try testing.expect_equal(trap_id, outcome.trap?)
|
||||
} else {
|
||||
try testing.expect(outcome.trap == null)
|
||||
byte :: evaluation_byte(&outcome, &module, outcome.value, 0)
|
||||
try testing.expect(byte != null)
|
||||
try testing.expect_equal(u64(42), byte?.data.unsigned)
|
||||
try testing.expect(evaluation_byte(&outcome, &module, outcome.value, 1) == null)
|
||||
}
|
||||
deinit(&outcome)
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
succeeded = true
|
||||
break
|
||||
}
|
||||
try testing.expect(succeeded)
|
||||
}
|
||||
}
|
||||
|
||||
runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
types [1]ir.Type :: [.int]
|
||||
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
return_value(&module, main, ir.ValueId(0))
|
||||
@@ -523,9 +622,8 @@ add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.A
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError {
|
||||
if (instruction.type == .address) module.functions.items[usize(function)].has_addresses = true
|
||||
try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction)
|
||||
emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
_ = try ir.emit(module, function, block, instruction)
|
||||
}
|
||||
|
||||
branching_recursive_calls_and_lazy_initializers_resume_the_current_block test {
|
||||
@@ -534,7 +632,7 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
count :: try add_function(&module, types[..], 1, .int, 9)
|
||||
count :: try add_function(&module, types[..], 1, .int)
|
||||
done :: try add_block(&module, count)
|
||||
recurse :: try add_block(&module, count)
|
||||
try emit(&module, count, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
@@ -552,7 +650,7 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test {
|
||||
try emit_at(&module, count, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(4) })
|
||||
module.functions.items[usize(count)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(7) }
|
||||
|
||||
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||
initializer :: try add_function(&module, &[], 0, .int)
|
||||
init_entry :: try add_block(&module, initializer)
|
||||
init_exit :: try add_block(&module, initializer)
|
||||
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
|
||||
@@ -566,7 +664,7 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test {
|
||||
module.functions.items[usize(initializer)].blocks.items[usize(init_exit)].terminator = ir.Terminator{ value = ir.ValueId(2) }
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
main_entry :: try add_block(&module, main)
|
||||
main_exit :: try add_block(&module, main)
|
||||
module.entry = main
|
||||
@@ -611,7 +709,7 @@ all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
|
||||
main :: try add_function(&module, &[], 0, .bool)
|
||||
module.entry = main
|
||||
left_value := ir.Value{ type = type, data = ir.ValueData{ integer = left } }
|
||||
right_value := ir.Value{ type = type, data = ir.ValueData{ integer = right } }
|
||||
@@ -650,7 +748,7 @@ nan_comparisons_obey_ieee_rules_in_either_operand_position test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
|
||||
main :: try add_function(&module, &[], 0, .bool)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{
|
||||
op = .constant, type = .float, destination = ir.ValueId(0),
|
||||
@@ -680,7 +778,7 @@ boolean_equality_and_branching_use_boolean_payloads test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
yes :: try add_block(&module, main)
|
||||
no :: try add_block(&module, main)
|
||||
@@ -710,7 +808,7 @@ assign_local_replaces_initialized_values_without_producing_a_result test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
main :: try add_function(&module, types[..], 0, .int, 3)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, 1))
|
||||
try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) })
|
||||
@@ -735,7 +833,7 @@ reset_local_invalidates_reads_and_assignments_at_the_instruction_span test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
main :: try add_function(&module, types[..], 0, .int, 2)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
span :: diagnostics.Span{ start = 21, end = 26 }
|
||||
try emit(&module, main, integer(0, 42))
|
||||
@@ -758,7 +856,7 @@ premature_assignment_and_reset_traps_release_frames_at_all_allocation_boundaries
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
span :: diagnostics.Span{ start = 14, end = 19 }
|
||||
try emit(&module, main, integer(0, 42))
|
||||
@@ -793,7 +891,7 @@ assignment_diagnostic_allocation_failure_releases_runtime_storage test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
types [1]ir.Type :: [.int]
|
||||
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||
main :: try add_function(&module, types[..], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, 42))
|
||||
try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(0) })
|
||||
@@ -825,7 +923,7 @@ logical_not_inverts_boolean_payload_and_composes_in_typed_ir test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
|
||||
main :: try add_function(&module, &[], 0, .bool)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{
|
||||
op = .constant,
|
||||
@@ -859,27 +957,12 @@ unsigned proc(destination u32, value u64) ir.Instruction {
|
||||
}
|
||||
|
||||
@hide:file
|
||||
add_array proc(module @mut ir.Module, element ir.Type, child ir.ArrayId, len usize) ir.ArrayId ! mem.AllocError {
|
||||
width :: if (element == .array) module.arrays.items[usize(child)].cells else 1
|
||||
id :: ir.ArrayId(module.arrays.items.len)
|
||||
try arraylist.append(&module.arrays, ir.ArrayType{
|
||||
element = element, element_array = child, len = len, cells = len * width, mutable = true,
|
||||
})
|
||||
return id
|
||||
add_array proc(module @mut ir.Module, element ir.Type, child ir.ArrayId, len usize) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
return try ir.add_array(module, element, child, len, true)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
array_storage proc(module @mut ir.Module, function ir.FunctionId, index u32, shape ir.ArrayId, local bool) void ! mem.AllocError {
|
||||
body :: &module.functions.items[usize(function)]
|
||||
try arraylist.append(&body.array_storage, ir.ArrayStorage{
|
||||
index = index, array_type = shape, offset = body.aggregate_cells, local = local,
|
||||
})
|
||||
body.aggregate_cells += module.arrays.items[usize(shape)].cells
|
||||
}
|
||||
|
||||
@hide:file
|
||||
literal proc(module @mut ir.Module, function ir.FunctionId, destination u32, shape ir.ArrayId, elements []u32) void ! mem.AllocError {
|
||||
try array_storage(module, function, destination, shape, false)
|
||||
literal proc(module @mut ir.Module, function ir.FunctionId, destination u32, shape ir.ArrayId, elements []u32) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
block :: &module.functions.items[usize(function)].blocks.items[0]
|
||||
start :: block.arguments.items.len
|
||||
for (elements) |element| try arraylist.append(&block.arguments, ir.ValueId(element))
|
||||
@@ -896,12 +979,12 @@ nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test {
|
||||
defer diagnostics.deinit(&store)
|
||||
row :: try add_array(&module, .int, ir.no_array, 2)
|
||||
matrix :: try add_array(&module, .array, row, 2)
|
||||
types [2]ir.Type :: [.array, .array]
|
||||
main :: try add_function(&module, types[..], 0, .array, 15)
|
||||
main :: try add_function(&module, &[], 0, .array)
|
||||
_ = try ir.add_local(&module, main, .array, matrix)
|
||||
_ = try ir.add_local(&module, main, .array, matrix)
|
||||
module.entry = main
|
||||
module.functions.items[usize(main)].result_array_type = matrix
|
||||
try array_storage(&module, main, 0, matrix, true)
|
||||
try array_storage(&module, main, 1, matrix, true)
|
||||
|
||||
try emit(&module, main, integer(0, 1))
|
||||
try emit(&module, main, integer(1, 2))
|
||||
pair [2]u32 :: [0, 1]
|
||||
@@ -909,14 +992,14 @@ nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test {
|
||||
rows [2]u32 :: [2, 2]
|
||||
try literal(&module, main, 3, matrix, rows[..])
|
||||
try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(3) })
|
||||
try array_storage(&module, main, 4, matrix, false)
|
||||
|
||||
try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(4), slot = 0 })
|
||||
try emit(&module, main, ir.Instruction{ op = .store_local, slot = 1, left = ir.ValueId(4) })
|
||||
try emit(&module, main, unsigned(5, 0))
|
||||
try emit(&module, main, unsigned(6, 1))
|
||||
try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = matrix, destination = ir.ValueId(7), slot = 0 })
|
||||
try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .array, array_type = row, destination = ir.ValueId(8), left = ir.ValueId(7), right = ir.ValueId(5) })
|
||||
try array_storage(&module, main, 9, row, false)
|
||||
|
||||
try emit(&module, main, ir.Instruction{ op = .load_address, type = .array, array_type = row, destination = ir.ValueId(9), left = ir.ValueId(8) })
|
||||
try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(10), left = ir.ValueId(8), right = ir.ValueId(6) })
|
||||
try emit(&module, main, integer(11, 9))
|
||||
@@ -926,12 +1009,12 @@ nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test {
|
||||
try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(11) })
|
||||
try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .array, array_type = row, destination = ir.ValueId(8), left = ir.ValueId(7), right = ir.ValueId(6) })
|
||||
try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(8), right = ir.ValueId(9) })
|
||||
try array_storage(&module, main, 12, matrix, false)
|
||||
|
||||
try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(12), slot = 0 })
|
||||
try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 1, left = ir.ValueId(12) })
|
||||
try emit(&module, main, integer(13, 7))
|
||||
try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(13) })
|
||||
try array_storage(&module, main, 14, matrix, false)
|
||||
|
||||
try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(14), slot = 1 })
|
||||
return_value(&module, main, ir.ValueId(14))
|
||||
outcome := try run(mem.c_allocator, &module, &store)
|
||||
@@ -949,13 +1032,11 @@ array_arguments_and_returns_are_private_values_before_frame_release test {
|
||||
defer diagnostics.deinit(&store)
|
||||
shape :: try add_array(&module, .int, ir.no_array, 1)
|
||||
pair_shape :: try add_array(&module, .int, ir.no_array, 2)
|
||||
types [2]ir.Type :: [.array, .array]
|
||||
callee :: try add_function(&module, types[..], 1, .array, 6)
|
||||
callee :: try add_function(&module, &[], 0, .array)
|
||||
_ = try ir.add_parameter(&module, callee, .array, shape)
|
||||
_ = try ir.add_local(&module, callee, .array, shape)
|
||||
module.functions.items[usize(callee)].result_array_type = shape
|
||||
try array_storage(&module, callee, 0, shape, true)
|
||||
try array_storage(&module, callee, 1, shape, true)
|
||||
try array_storage(&module, callee, 0, shape, false)
|
||||
try array_storage(&module, callee, 5, shape, false)
|
||||
|
||||
try emit(&module, callee, ir.Instruction{ op = .load_local, type = .array, array_type = shape, destination = ir.ValueId(0), slot = 0 })
|
||||
try emit(&module, callee, ir.Instruction{ op = .store_local, left = ir.ValueId(0), slot = 1 })
|
||||
try emit(&module, callee, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(1), slot = 1 })
|
||||
@@ -970,14 +1051,14 @@ array_arguments_and_returns_are_private_values_before_frame_release test {
|
||||
try emit(&module, callee, integer(4, 7))
|
||||
try emit(&module, callee, ir.Instruction{ op = .store_address, left = ir.ValueId(3), right = ir.ValueId(4) })
|
||||
return_value(&module, callee, ir.ValueId(5))
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 8)
|
||||
main :: try add_function(&module, &[], 0, .array)
|
||||
module.entry = main
|
||||
module.functions.items[usize(main)].result_array_type = pair_shape
|
||||
try emit(&module, main, integer(0, 42))
|
||||
element [1]u32 :: [0]
|
||||
try literal(&module, main, 1, shape, element[..])
|
||||
try emit(&module, main, function_value(2, callee))
|
||||
try array_storage(&module, main, 3, shape, false)
|
||||
|
||||
try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1))
|
||||
try emit(&module, main, ir.Instruction{ op = .call, type = .array, array_type = shape, destination = ir.ValueId(3), left = ir.ValueId(2), arguments_start = 1, arguments_count = 1 })
|
||||
try emit(&module, main, unsigned(4, 0))
|
||||
@@ -999,11 +1080,11 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
shape :: try add_array(&module, .int, ir.no_array, 1)
|
||||
first :: try add_function(&module, mem.empty(ir.Type), 0, .array, 1)
|
||||
second :: try add_function(&module, mem.empty(ir.Type), 0, .array, 2)
|
||||
first :: try add_function(&module, &[], 0, .array)
|
||||
second :: try add_function(&module, &[], 0, .array)
|
||||
module.functions.items[usize(first)].result_array_type = shape
|
||||
module.functions.items[usize(second)].result_array_type = shape
|
||||
try array_storage(&module, first, 0, shape, false)
|
||||
|
||||
try emit(&module, first, ir.Instruction{ op = .load_global, type = .array, array_type = shape, destination = ir.ValueId(0), slot = 1 })
|
||||
return_value(&module, first, ir.ValueId(0))
|
||||
try emit(&module, second, integer(0, 42))
|
||||
@@ -1017,11 +1098,11 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t
|
||||
span = diagnostics.Span{ start = 0, end = 0 },
|
||||
})
|
||||
}
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 2)
|
||||
main :: try add_function(&module, &[], 0, .array)
|
||||
module.entry = main
|
||||
module.functions.items[usize(main)].result_array_type = shape
|
||||
try emit(&module, main, ir.Instruction{ op = .address_global, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(0), slot = 0 })
|
||||
try array_storage(&module, main, 1, shape, false)
|
||||
|
||||
try emit(&module, main, ir.Instruction{ op = .load_address, type = .array, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
return_value(&module, main, ir.ValueId(1))
|
||||
succeeded := false
|
||||
@@ -1049,11 +1130,18 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t
|
||||
try testing.expect(failures > 0)
|
||||
# restarting a run clears global readiness, including address-triggered initialization cycles.
|
||||
cycle_span :: diagnostics.Span{ start = 51, end = 59 }
|
||||
module.functions.items[usize(second)].has_addresses = true
|
||||
module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = ir.Instruction{
|
||||
cycle_initializer :: try add_function(&module, &[], 0, .array)
|
||||
module.functions.items[usize(cycle_initializer)].result_array_type = shape
|
||||
try emit(&module, cycle_initializer, ir.Instruction{
|
||||
op = .address_global, type = .address, pointee = .array, array_type = shape,
|
||||
destination = ir.ValueId(0), slot = 0, span = cycle_span,
|
||||
}
|
||||
})
|
||||
try emit(&module, cycle_initializer, ir.Instruction{
|
||||
op = .load_address, type = .array, array_type = shape,
|
||||
destination = ir.ValueId(1), left = ir.ValueId(0),
|
||||
})
|
||||
return_value(&module, cycle_initializer, ir.ValueId(1))
|
||||
module.globals.items[1].initializer = cycle_initializer
|
||||
state := AllocationState{ remaining = 64 }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
cycle := try run(allocator, &module, &store)
|
||||
@@ -1063,7 +1151,7 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
trap_id :: try diagnostics.add(&store, .invalid_escape, cycle_span)
|
||||
module.functions.items[usize(second)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 42)
|
||||
module.globals.items[1].initializer = second
|
||||
trapped := try run(allocator, &module, &store)
|
||||
defer deinit(&trapped)
|
||||
try testing.expect_equal(trap_id, trapped.trap?)
|
||||
@@ -1083,10 +1171,10 @@ array_bounds_check_zero_lengths_and_the_full_unsigned_word_before_addressing tes
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
shape :: try add_array(&module, .int, ir.no_array, length)
|
||||
types [1]ir.Type :: [.array]
|
||||
main :: try add_function(&module, types[..], 0, .int, 5)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
_ = try ir.add_local(&module, main, .array, shape)
|
||||
module.entry = main
|
||||
try array_storage(&module, main, 0, shape, true)
|
||||
|
||||
try emit(&module, main, integer(0, 42))
|
||||
operands [1]u32 :: [0]
|
||||
try literal(&module, main, 1, shape, operands[..length])
|
||||
@@ -1129,10 +1217,10 @@ zero_leaf_nested_arrays_return_a_shape_without_an_owned_allocation test {
|
||||
defer diagnostics.deinit(&store)
|
||||
empty :: try add_array(&module, .bool, ir.no_array, 0)
|
||||
rows :: try add_array(&module, .array, empty, 3)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 4)
|
||||
main :: try add_function(&module, &[], 0, .array)
|
||||
module.entry = main
|
||||
module.functions.items[usize(main)].result_array_type = rows
|
||||
try literal(&module, main, 0, empty, mem.empty(u32))
|
||||
try literal(&module, main, 0, empty, &[])
|
||||
operands [3]u32 :: [0, 0, 0]
|
||||
try literal(&module, main, 1, rows, operands[..])
|
||||
return_value(&module, main, ir.ValueId(1))
|
||||
@@ -1148,7 +1236,7 @@ zero_leaf_nested_arrays_return_a_shape_without_an_owned_allocation test {
|
||||
deinit(&outcome)
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
try emit(&module, main, unsigned(2, 2))
|
||||
try array_storage(&module, main, 3, empty, false)
|
||||
|
||||
try emit(&module, main, ir.Instruction{ op = .index, type = .array, array_type = empty, destination = ir.ValueId(3), left = ir.ValueId(1), right = ir.ValueId(2) })
|
||||
return_value(&module, main, ir.ValueId(3))
|
||||
module.functions.items[usize(main)].result_array_type = empty
|
||||
@@ -1172,7 +1260,7 @@ UnsignedCase :: struct {
|
||||
unsigned_arithmetic_and_comparisons_preserve_all_sixty_four_bits test {
|
||||
wide_sum u64 :: 9223372036854775815
|
||||
almost_maximum u64 :: 18446744073709551614
|
||||
cases [10]UnsignedCase :: [
|
||||
cases [11]UnsignedCase :: [
|
||||
UnsignedCase{ op = .add, left = maxval!(u64), right = 0, expected = maxval!(u64) },
|
||||
UnsignedCase{ op = .add, left = maxval!(u64), right = 1, expected = null },
|
||||
UnsignedCase{ op = .add, left = 9223372036854775808, right = 7, expected = wide_sum },
|
||||
@@ -1183,13 +1271,14 @@ unsigned_arithmetic_and_comparisons_preserve_all_sixty_four_bits test {
|
||||
UnsignedCase{ op = .multiply, left = maxval!(u64), right = 0, expected = u64(0) },
|
||||
UnsignedCase{ op = .multiply, left = 0, right = maxval!(u64), expected = u64(0) },
|
||||
UnsignedCase{ op = .multiply, left = 3, right = 14, expected = u64(42) },
|
||||
UnsignedCase{ op = .remainder, left = maxval!(u64), right = 10, expected = u64(5) },
|
||||
]
|
||||
for (cases) |case| {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .usize, 3)
|
||||
main :: try add_function(&module, &[], 0, .usize)
|
||||
module.entry = main
|
||||
span :: diagnostics.Span{ start = 7, end = 12 }
|
||||
try emit(&module, main, unsigned(0, case.left))
|
||||
@@ -1211,7 +1300,7 @@ unsigned_arithmetic_and_comparisons_preserve_all_sixty_four_bits test {
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
|
||||
main :: try add_function(&module, &[], 0, .bool)
|
||||
module.entry = main
|
||||
try emit(&module, main, unsigned(0, maxval!(u64)))
|
||||
try emit(&module, main, unsigned(1, 1))
|
||||
@@ -1244,11 +1333,11 @@ all_scalar_leaf_families_survive_aggregate_return_and_function_extraction test {
|
||||
}
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
target :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1)
|
||||
target :: try add_function(&module, &[], 0, .int)
|
||||
try emit(&module, target, integer(0, 42))
|
||||
return_value(&module, target, ir.ValueId(0))
|
||||
shape :: try add_array(&module, scalar.type, ir.no_array, 1)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 5)
|
||||
main :: try add_function(&module, &[], 0, .array)
|
||||
module.entry = main
|
||||
module.functions.items[usize(main)].result_array_type = shape
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = scalar.type, destination = ir.ValueId(0), constant = scalar })
|
||||
@@ -1288,10 +1377,10 @@ array_address_initialization_flags_survive_reset_and_diagnostic_allocation_failu
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
shape :: try add_array(&module, .int, ir.no_array, 1)
|
||||
types [1]ir.Type :: [.array]
|
||||
main :: try add_function(&module, types[..], 0, .int, 5)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
_ = try ir.add_local(&module, main, .array, shape)
|
||||
module.entry = main
|
||||
try array_storage(&module, main, 0, shape, true)
|
||||
|
||||
try emit(&module, main, integer(0, 42))
|
||||
operand [1]u32 :: [0]
|
||||
try literal(&module, main, 1, shape, operand[..])
|
||||
@@ -1301,7 +1390,8 @@ array_address_initialization_flags_survive_reset_and_diagnostic_allocation_failu
|
||||
try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(4), left = ir.ValueId(2), right = ir.ValueId(3) })
|
||||
try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 })
|
||||
span :: diagnostics.Span{ start = 23, end = 37 }
|
||||
try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = ir.ValueId(0), left = ir.ValueId(4), right = ir.ValueId(0), span = span })
|
||||
destination :: if (op == .load_address) ir.ValueId(0) else ir.no_value
|
||||
try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = destination, left = ir.ValueId(4), right = ir.ValueId(0), span = span })
|
||||
for (usize(0)..usize(2)) |attempt| {
|
||||
state := AllocationState{ remaining = 64 }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
@@ -1332,10 +1422,11 @@ aggregate_loop_places_reuse_storage_without_allocation_per_iteration test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
shape :: try add_array(&module, .int, ir.no_array, 1)
|
||||
types [2]ir.Type :: [.array, .usize]
|
||||
main :: try add_function(&module, types[..], 0, .int, 14)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
_ = try ir.add_local(&module, main, .array, shape)
|
||||
_ = try ir.add_local(&module, main, .usize, ir.no_array)
|
||||
module.entry = main
|
||||
try array_storage(&module, main, 0, shape, true)
|
||||
|
||||
try emit(&module, main, integer(0, 0))
|
||||
operand [1]u32 :: [0]
|
||||
try literal(&module, main, 1, shape, operand[..])
|
||||
@@ -1389,9 +1480,10 @@ recursive_array_backings_and_addresses_remain_fresh_across_stack_growth test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
shape :: try add_array(&module, .int, ir.no_array, 1)
|
||||
types [2]ir.Type :: [.int, .array]
|
||||
recursive :: try add_function(&module, types[..], 1, .int, 12)
|
||||
try array_storage(&module, recursive, 1, shape, true)
|
||||
recursive :: try add_function(&module, &[], 0, .int)
|
||||
_ = try ir.add_parameter(&module, recursive, .int, ir.no_array)
|
||||
_ = try ir.add_local(&module, recursive, .array, shape)
|
||||
|
||||
try emit(&module, recursive, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
operand [1]u32 :: [0]
|
||||
try literal(&module, recursive, 1, shape, operand[..])
|
||||
@@ -1413,7 +1505,7 @@ recursive_array_backings_and_addresses_remain_fresh_across_stack_growth test {
|
||||
try emit_at(&module, recursive, recurse, ir.Instruction{ op = .load_address, type = .int, destination = ir.ValueId(10), left = ir.ValueId(4) })
|
||||
try emit_at(&module, recursive, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(11), left = ir.ValueId(9), right = ir.ValueId(10) })
|
||||
module.functions.items[usize(recursive)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(11) }
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||
main :: try add_function(&module, &[], 0, .int)
|
||||
module.entry = main
|
||||
try emit(&module, main, function_value(0, recursive))
|
||||
try emit(&module, main, integer(1, 64))
|
||||
@@ -1432,7 +1524,7 @@ inclusive_unsigned_range_reaches_the_maximum_without_incrementing_past_it test {
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [2]ir.Type :: [.usize, .usize]
|
||||
main :: try add_function(&module, types[..], 0, .usize, 9)
|
||||
main :: try add_function(&module, types[..], 0, .usize)
|
||||
module.entry = main
|
||||
try emit(&module, main, unsigned(0, maxval!(u64) - 1))
|
||||
try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) })
|
||||
@@ -1463,3 +1555,126 @@ inclusive_unsigned_range_reaches_the_maximum_without_incrementing_past_it test {
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(u64(2), outcome.value.data.unsigned)
|
||||
}
|
||||
|
||||
union_storage_overlays_nested_record_and_array_payloads_and_copies_by_value test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
record_fields [2]ir.RecordField :: [ir.RecordField{ type = .u8 }, ir.RecordField{ type = .u32 }]
|
||||
record :: try ir.add_record(&module, record_fields[..])
|
||||
bytes :: try add_array(&module, .u8, ir.no_array, 8)
|
||||
variants [2]ir.RecordField :: [
|
||||
ir.RecordField{ type = .record, array_type = record },
|
||||
ir.RecordField{ type = .array, array_type = bytes },
|
||||
]
|
||||
shape :: try ir.add_union(&module, variants[..], false)
|
||||
identity :: try add_function(&module, &[], 0, .record)
|
||||
_ = try ir.add_parameter(&module, identity, .record, shape)
|
||||
module.functions.items[usize(identity)].result_array_type = shape
|
||||
try emit(&module, identity, ir.Instruction{ op = .load_local, type = .record, array_type = shape, destination = ir.ValueId(0) })
|
||||
return_value(&module, identity, ir.ValueId(0))
|
||||
main :: try add_function(&module, &[], 0, .u32)
|
||||
_ = try ir.add_local(&module, main, .record, shape)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .u8, destination = ir.ValueId(0), constant = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 1 } } })
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .u32, destination = ir.ValueId(1), constant = ir.Value{ type = .u32, data = ir.ValueData{ unsigned = 287454020 } } })
|
||||
block :: &module.functions.items[usize(main)].blocks.items[0]
|
||||
try arraylist.append(&block.arguments, ir.ValueId(0))
|
||||
try arraylist.append(&block.arguments, ir.ValueId(1))
|
||||
try emit(&module, main, ir.Instruction{ op = .record_literal, type = .record, array_type = record, destination = ir.ValueId(2), arguments_count = 2 })
|
||||
try emit(&module, main, ir.Instruction{ op = .union_literal, type = .record, array_type = shape, destination = ir.ValueId(3), left = ir.ValueId(2) })
|
||||
try emit(&module, main, ir.Instruction{ op = .store_local, left = ir.ValueId(3) })
|
||||
try emit(&module, main, function_value(4, identity))
|
||||
try arraylist.append(&block.arguments, ir.ValueId(3))
|
||||
try emit(&module, main, ir.Instruction{ op = .call, type = .record, array_type = shape, destination = ir.ValueId(5), left = ir.ValueId(4), arguments_start = 2, arguments_count = 1 })
|
||||
try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .record, array_type = shape, destination = ir.ValueId(6) })
|
||||
try emit(&module, main, ir.Instruction{ op = .field_address, type = .address, pointee = .array, array_type = bytes, destination = ir.ValueId(7), left = ir.ValueId(6), slot = 1 })
|
||||
try emit(&module, main, unsigned(8, 4))
|
||||
try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .u8, destination = ir.ValueId(9), left = ir.ValueId(7), right = ir.ValueId(8) })
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .u8, destination = ir.ValueId(10), constant = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 153 } } })
|
||||
try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(9), right = ir.ValueId(10) })
|
||||
try emit(&module, main, ir.Instruction{ op = .load_local, type = .record, array_type = shape, destination = ir.ValueId(11) })
|
||||
try emit(&module, main, ir.Instruction{ op = .field, type = .record, array_type = record, destination = ir.ValueId(12), left = ir.ValueId(11) })
|
||||
try emit(&module, main, ir.Instruction{ op = .field, type = .u32, destination = ir.ValueId(13), left = ir.ValueId(12), slot = 1 })
|
||||
try emit(&module, main, ir.Instruction{ op = .field, type = .record, array_type = record, destination = ir.ValueId(14), left = ir.ValueId(5) })
|
||||
try emit(&module, main, ir.Instruction{ op = .field, type = .u32, destination = ir.ValueId(15), left = ir.ValueId(14), slot = 1 })
|
||||
try emit(&module, main, ir.Instruction{ op = .subtract, type = .u32, destination = ir.ValueId(16), left = ir.ValueId(13), right = ir.ValueId(15) })
|
||||
return_value(&module, main, ir.ValueId(16))
|
||||
outcome := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(u64(85), outcome.value.data.unsigned)
|
||||
}
|
||||
|
||||
tagged_unions_preserve_actual_tags_and_trap_inactive_value_and_address_projections test {
|
||||
operations [2]ir.Op :: [.field, .field_address]
|
||||
for (operations) |operation| {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
tag :: ir.Value{ type = .i8, data = ir.ValueData{ integer = -1 } }
|
||||
variants [2]ir.RecordField :: [
|
||||
ir.RecordField{ type = .int, tag = tag },
|
||||
ir.RecordField{ type = .int, tag = ir.Value{ type = .i8, data = ir.ValueData{ integer = 37 } } },
|
||||
]
|
||||
shape :: try ir.add_union(&module, variants[..], true)
|
||||
main :: try add_function(&module, &[], 0, .i8)
|
||||
_ = try ir.add_local(&module, main, .record, shape)
|
||||
module.entry = main
|
||||
try emit(&module, main, integer(0, 42))
|
||||
try emit(&module, main, ir.Instruction{ op = .union_literal, type = .record, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0), constant = tag })
|
||||
try emit(&module, main, ir.Instruction{ op = .union_tag, type = .i8, destination = ir.ValueId(2), left = ir.ValueId(1) })
|
||||
return_value(&module, main, ir.ValueId(2))
|
||||
outcome := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(-1), outcome.value.data.integer)
|
||||
base := ir.ValueId(1)
|
||||
if (operation == .field_address) {
|
||||
try emit(&module, main, ir.Instruction{ op = .store_local, left = base })
|
||||
try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .record, array_type = shape, destination = ir.ValueId(3) })
|
||||
base = ir.ValueId(3)
|
||||
}
|
||||
span :: diagnostics.Span{ start = 11, end = 15 }
|
||||
type :: if (operation == .field) ir.Type.int else ir.Type.address
|
||||
try emit(&module, main, ir.Instruction{ op = operation, type = type, pointee = .int, destination = ir.ValueId(4), left = base, slot = 1, span = span })
|
||||
trapped := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&trapped)
|
||||
try expect_trap(trapped, &store, .inactive_union_field, span)
|
||||
}
|
||||
}
|
||||
|
||||
union_scalar_reinterpretation_and_zero_leaf_payloads_keep_their_shapes test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
empty :: try ir.add_record(&module, &[])
|
||||
variants [3]ir.RecordField :: [
|
||||
ir.RecordField{ type = .float },
|
||||
ir.RecordField{ type = .u64 },
|
||||
ir.RecordField{ type = .record, array_type = empty },
|
||||
]
|
||||
shape :: try ir.add_union(&module, variants[..], false)
|
||||
main :: try add_function(&module, &[], 0, .u64)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 1.0 } } })
|
||||
try emit(&module, main, ir.Instruction{ op = .union_literal, type = .record, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
try emit(&module, main, ir.Instruction{ op = .field, type = .u64, destination = ir.ValueId(2), left = ir.ValueId(1), slot = 1 })
|
||||
return_value(&module, main, ir.ValueId(2))
|
||||
bits := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&bits)
|
||||
try testing.expect(bits.trap == null)
|
||||
try testing.expect_equal(u64(4607182418800017408), bits.value.data.unsigned)
|
||||
try emit(&module, main, ir.Instruction{ op = .field, type = .record, array_type = empty, destination = ir.ValueId(3), left = ir.ValueId(1), slot = 2 })
|
||||
module.functions.items[usize(main)].result = .record
|
||||
module.functions.items[usize(main)].result_array_type = empty
|
||||
return_value(&module, main, ir.ValueId(3))
|
||||
zero := try run(mem.c_allocator, &module, &store)
|
||||
defer deinit(&zero)
|
||||
try testing.expect(zero.trap == null)
|
||||
try testing.expect_equal(empty, zero.value.array_type)
|
||||
try testing.expect_equal(usize(0), zero.value.data.elements.len)
|
||||
}
|
||||
|
||||
+471
-25
@@ -1,5 +1,7 @@
|
||||
import "@std/mem"
|
||||
import "@std/arraylist"
|
||||
import "@std/debug"
|
||||
import "@std/enummap"
|
||||
import "@source/diagnostics"
|
||||
|
||||
FunctionId :: distinct u32
|
||||
@@ -13,20 +15,67 @@ no_array :: maxval!(ArrayId)
|
||||
no_value :: maxval!(ValueId)
|
||||
|
||||
Type :: enum {
|
||||
invalid,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
string,
|
||||
void,
|
||||
function,
|
||||
array,
|
||||
usize,
|
||||
address,
|
||||
invalid
|
||||
int
|
||||
float
|
||||
bool
|
||||
string
|
||||
void
|
||||
function
|
||||
array
|
||||
record
|
||||
address
|
||||
pointer
|
||||
i8, i16, i32, isize
|
||||
u8, u16, u32, u64, usize
|
||||
}
|
||||
|
||||
aggregate proc(type Type) bool {
|
||||
return type == .array or type == .record
|
||||
}
|
||||
|
||||
@hide:file
|
||||
integer_widths :: $enummap.init_default(Type, usize, usize(0), {
|
||||
i8 = 8, u8 = 8,
|
||||
i16 = 16, u16 = 16,
|
||||
i32 = 32, u32 = 32,
|
||||
int = 64, isize = 64, usize = 64, u64 = 64,
|
||||
})
|
||||
|
||||
#! integer storage widths for the supported 64-bit target; zero for noninteger types.
|
||||
integer_bits proc(type Type) usize {
|
||||
if (enummap.get(&integer_widths, type)) |width| return width
|
||||
return 0
|
||||
}
|
||||
|
||||
unsigned_integer proc(type Type) bool {
|
||||
return type == .u8 or type == .u16 or type == .u32 or type == .u64 or type == .usize
|
||||
}
|
||||
|
||||
#! maxima for supported integer storage types; noninteger keys are absent.
|
||||
integer_maxima :: $enummap.init_default(Type, u64, u64(0), {
|
||||
i8 = 127,
|
||||
i16 = 32767,
|
||||
i32 = 2147483647,
|
||||
int = u64(maxval!(i64)), isize = u64(maxval!(i64)),
|
||||
u8 = 255,
|
||||
u16 = 65535,
|
||||
u32 = 4294967295,
|
||||
u64 = maxval!(u64), usize = maxval!(u64),
|
||||
})
|
||||
|
||||
#! minima for supported integer storage types; noninteger keys are absent.
|
||||
integer_minima :: $enummap.init_default(Type, i64, i64(0), {
|
||||
i8 = i64(-128),
|
||||
i16 = i64(-32768),
|
||||
i32 = i64(-2147483648),
|
||||
int = minval!(i64), isize = minval!(i64),
|
||||
u8 = 0, u16 = 0, u32 = 0, u64 = 0, usize = 0,
|
||||
})
|
||||
|
||||
ValueData :: union {
|
||||
integer i64
|
||||
# source pointers use run-wide handles; internal addresses use frame-local IDs below.
|
||||
unsigned u64
|
||||
real f64
|
||||
boolean bool
|
||||
@@ -38,16 +87,35 @@ ValueData :: union {
|
||||
Value :: struct {
|
||||
type Type = .void
|
||||
array_type ArrayId = no_array
|
||||
raw bool = false
|
||||
# interpreter poison survives copies; native storage uses LLVM poison directly.
|
||||
uninitialized bool = false
|
||||
data ValueData = ValueData{ integer = 0 }
|
||||
}
|
||||
|
||||
#! fixed recursive shape; `cells` counts flattened scalar leaves, including zero-sized children.
|
||||
RecordField :: struct {
|
||||
type Type
|
||||
array_type ArrayId = no_array
|
||||
offset usize = 0
|
||||
byte_offset usize = 0
|
||||
tag Value = Value{}
|
||||
}
|
||||
|
||||
#! fixed recursive aggregate shape; cells count interpreter slots, byte_size describes native layout.
|
||||
#! unions retain overlapping byte cells; ordinary aggregates retain flattened scalar leaves.
|
||||
ArrayType :: struct {
|
||||
element Type
|
||||
kind Type = .array
|
||||
element Type = .invalid
|
||||
element_array ArrayId = no_array
|
||||
len usize
|
||||
cells usize
|
||||
mutable bool
|
||||
len usize = 0
|
||||
cells usize = 0
|
||||
mutable bool = false
|
||||
fields []mut RecordField = &[]
|
||||
is_union bool = false
|
||||
tag_type Type = .void
|
||||
payload_offset usize = 0
|
||||
byte_size usize = 0
|
||||
alignment usize = 1
|
||||
}
|
||||
|
||||
#! maps a local slot or temporary to its preallocated, by-value frame segment.
|
||||
@@ -60,46 +128,64 @@ ArrayStorage :: struct {
|
||||
|
||||
Op :: enum {
|
||||
constant
|
||||
undefined_value
|
||||
load_local
|
||||
store_local
|
||||
assign_local
|
||||
reset_local
|
||||
load_global
|
||||
array_literal
|
||||
record_literal
|
||||
union_literal
|
||||
union_tag
|
||||
field
|
||||
index
|
||||
address_local
|
||||
address_global
|
||||
index_address
|
||||
field_address
|
||||
load_address
|
||||
store_address
|
||||
pointer_from_address
|
||||
pointer_address
|
||||
pointer_offset
|
||||
pointer_cast
|
||||
string_pointer
|
||||
string_length
|
||||
slice_check
|
||||
sentinel_check
|
||||
optional_check
|
||||
memory_copy, memory_set
|
||||
negate
|
||||
convert
|
||||
logical_not
|
||||
add
|
||||
subtract
|
||||
multiply
|
||||
divide
|
||||
bit_not, bit_and, bit_or, bit_xor
|
||||
shift_left, shift_right, shift_left_saturating
|
||||
add, subtract
|
||||
multiply, divide, remainder
|
||||
call
|
||||
equal
|
||||
not_equal
|
||||
less
|
||||
less_equal
|
||||
greater
|
||||
greater_equal
|
||||
equal, not_equal
|
||||
less, less_equal
|
||||
greater, greater_equal
|
||||
}
|
||||
|
||||
Instruction :: struct {
|
||||
op Op
|
||||
type Type = .void
|
||||
# pointer operations describe their pointee here, not aggregate result storage.
|
||||
array_type ArrayId = no_array
|
||||
pointee Type = .invalid
|
||||
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
||||
destination ValueId = no_value
|
||||
left ValueId = no_value
|
||||
right ValueId = no_value
|
||||
extra ValueId = no_value
|
||||
slot u32 = 0
|
||||
constant Value = Value{}
|
||||
arguments_start usize = 0
|
||||
arguments_count usize = 0
|
||||
variadic bool = false
|
||||
fixed_arguments usize = 0
|
||||
}
|
||||
|
||||
TerminatorKind :: enum {
|
||||
@@ -107,6 +193,7 @@ TerminatorKind :: enum {
|
||||
trap,
|
||||
jump,
|
||||
branch,
|
||||
unreachable,
|
||||
}
|
||||
|
||||
Terminator :: struct {
|
||||
@@ -124,11 +211,17 @@ Block :: struct {
|
||||
terminator Terminator = Terminator{}
|
||||
}
|
||||
|
||||
#! construction derives storage segments, aggregate totals, value counts, and address presence.
|
||||
#! producers use add_local, add_parameter, and emit; derived metadata is read-only to callers.
|
||||
Function :: struct {
|
||||
parameters arraylist.ArrayList(Type)
|
||||
# owned by the module allocator; empty for interpreted/native-defined functions.
|
||||
external_name []u8 = ""
|
||||
variadic bool = false
|
||||
locals arraylist.ArrayList(Type)
|
||||
result Type = .void
|
||||
result_array_type ArrayId = no_array
|
||||
result_fallible bool = false
|
||||
array_storage arraylist.ArrayList(ArrayStorage)
|
||||
aggregate_cells usize = 0
|
||||
has_addresses bool = false
|
||||
@@ -163,6 +256,7 @@ Module :: struct {
|
||||
entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
||||
}
|
||||
|
||||
#! retains `allocator` for executable storage; release the returned module with deinit.
|
||||
init proc(allocator mem.Allocator) Module {
|
||||
return Module{
|
||||
allocator = allocator,
|
||||
@@ -173,6 +267,7 @@ init proc(allocator mem.Allocator) Module {
|
||||
}
|
||||
}
|
||||
|
||||
#! retains `allocator` in its lists; transfer the function to a module using the same allocator for cleanup with deinit.
|
||||
init_function proc(allocator mem.Allocator) Function {
|
||||
return Function{
|
||||
parameters = arraylist.init(allocator),
|
||||
@@ -182,6 +277,7 @@ init_function proc(allocator mem.Allocator) Function {
|
||||
}
|
||||
}
|
||||
|
||||
#! retains `allocator` in its lists; transfer the block to a module function for cleanup with deinit.
|
||||
init_block proc(allocator mem.Allocator) Block {
|
||||
return Block{
|
||||
instructions = arraylist.init(allocator),
|
||||
@@ -189,6 +285,352 @@ init_block proc(allocator mem.Allocator) Block {
|
||||
}
|
||||
}
|
||||
|
||||
#! adds a fixed shape and derives its flattened storage size; child shapes must already exist.
|
||||
#! scalar elements use no_array; zero-leaf children retain their logical lengths.
|
||||
add_array proc(
|
||||
module @mut Module,
|
||||
element Type,
|
||||
element_array ArrayId,
|
||||
len usize,
|
||||
mutable bool,
|
||||
) ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
debug.assert(aggregate(element) == (element_array != no_array))
|
||||
width :: if (aggregate(element)) module.arrays.items[usize(element_array)].cells else 1
|
||||
if (width != 0 and len > divtrunc!(maxval!(usize), width)) return .storage_limit_reached
|
||||
cells :: len * width
|
||||
if (cells > divtrunc!(maxval!(usize), sizeof!(Value))) return .storage_limit_reached
|
||||
if (module.arrays.items.len >= usize(no_array)) return .storage_limit_reached
|
||||
stride :: storage_size(module, element, element_array)
|
||||
if (stride != 0 and len > divtrunc!(maxval!(usize), stride)) return .storage_limit_reached
|
||||
id :: ArrayId(module.arrays.items.len)
|
||||
try arraylist.append(&module.arrays, ArrayType{
|
||||
element = element,
|
||||
element_array = element_array,
|
||||
len = len,
|
||||
cells = cells,
|
||||
mutable = mutable,
|
||||
byte_size = len * stride,
|
||||
alignment = storage_alignment(module, element, element_array),
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
#! copies declaration-order fields and derives their flattened offsets; child shapes must exist.
|
||||
add_record proc(
|
||||
module @mut Module,
|
||||
fields []RecordField,
|
||||
) ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
if (module.arrays.items.len >= usize(no_array)) return .storage_limit_reached
|
||||
owned :: try mem.alloc(RecordField, module.allocator, fields.len)
|
||||
errdefer mem.free(module.allocator, owned)
|
||||
cells usize := 0
|
||||
bytes usize := 0
|
||||
alignment usize := 1
|
||||
for (fields) |field, index| {
|
||||
debug.assert(aggregate(field.type) == (field.array_type != no_array))
|
||||
width :: if (aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
|
||||
if (width > divtrunc!(maxval!(usize), sizeof!(Value)) - cells) return .storage_limit_reached
|
||||
field_alignment :: storage_alignment(module, field.type, field.array_type)
|
||||
if (field_alignment > alignment) alignment = field_alignment
|
||||
if (bytes > maxval!(usize) - (field_alignment - 1)) return .storage_limit_reached
|
||||
bytes = aligned_size(bytes, field_alignment)
|
||||
size :: storage_size(module, field.type, field.array_type)
|
||||
if (size > maxval!(usize) - bytes) return .storage_limit_reached
|
||||
owned[index] = RecordField{
|
||||
type = field.type, array_type = field.array_type, offset = cells,
|
||||
byte_offset = bytes, tag = field.tag,
|
||||
}
|
||||
bytes += size
|
||||
cells += width
|
||||
}
|
||||
id :: ArrayId(module.arrays.items.len)
|
||||
if (bytes > maxval!(usize) - (alignment - 1)) return .storage_limit_reached
|
||||
try arraylist.append(&module.arrays, ArrayType{
|
||||
kind = .record, fields = owned, cells = cells,
|
||||
byte_size = aligned_size(bytes, alignment), alignment = alignment,
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
#! returns a borrowed child, including a raw view when projecting overlapping union storage.
|
||||
aggregate_item proc(module @Module, value Value, index usize) Value {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
type := shape.element
|
||||
child := shape.element_array
|
||||
start usize := 0
|
||||
width usize := 1
|
||||
if (shape.kind == .record) {
|
||||
field :: shape.fields[index]
|
||||
type = field.type
|
||||
child = field.array_type
|
||||
start = field.offset
|
||||
if (value.raw) start = field.byte_offset
|
||||
if (shape.is_union) start = shape.payload_offset
|
||||
} else {
|
||||
if (value.raw) width = storage_size(module, type, child)
|
||||
else if (aggregate(type)) width = module.arrays.items[usize(child)].cells
|
||||
start = index * width
|
||||
}
|
||||
raw :: value.raw or shape.is_union
|
||||
if (!aggregate(type)) {
|
||||
if (raw) return read_storage(module, type, child, value.data.elements[start..], true)
|
||||
if (value.data.elements[start].uninitialized) return Value{ type = type, uninitialized = true }
|
||||
return value.data.elements[start]
|
||||
}
|
||||
width = if (raw) storage_size(module, type, child) else module.arrays.items[usize(child)].cells
|
||||
return Value{
|
||||
type = type, array_type = child, raw = raw,
|
||||
data = ValueData{ elements = value.data.elements[start..start + width] },
|
||||
}
|
||||
}
|
||||
|
||||
#! copies variant descriptors; all payloads share one eight-byte-aligned storage region.
|
||||
add_union proc(module @mut Module, fields []RecordField, tagged bool) ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
if (module.arrays.items.len >= usize(no_array)) return .storage_limit_reached
|
||||
owned :: try mem.alloc(RecordField, module.allocator, fields.len)
|
||||
errdefer mem.free(module.allocator, owned)
|
||||
size usize := 0
|
||||
for (fields) |field, index| {
|
||||
width :: storage_size(module, field.type, field.array_type)
|
||||
if (width > size) size = width
|
||||
owned[index] = field
|
||||
owned[index].offset = if (tagged) 8 else 0
|
||||
owned[index].byte_offset = owned[index].offset
|
||||
}
|
||||
prefix usize :: if (tagged) 8 else 0
|
||||
if (size > divtrunc!(maxval!(usize), sizeof!(Value)) - 15) return .storage_limit_reached
|
||||
size = aligned_size(size, 8) + prefix
|
||||
id :: ArrayId(module.arrays.items.len)
|
||||
tag_type :: if (tagged and fields.len != 0) fields[0].tag.type else Type.void
|
||||
try arraylist.append(&module.arrays, ArrayType{
|
||||
kind = .record, fields = owned, is_union = true,
|
||||
tag_type = tag_type,
|
||||
payload_offset = prefix, cells = size, byte_size = size, alignment = 8,
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
@hide:file
|
||||
aligned_size proc(size, alignment usize) usize {
|
||||
return divtrunc!(size + alignment - 1, alignment) * alignment
|
||||
}
|
||||
|
||||
@hide:file
|
||||
scalar_alignments :: $enummap.init_default(Type, usize, usize(1), {
|
||||
float = 8, string = 8, function = 8, pointer = 8,
|
||||
})
|
||||
|
||||
@hide:file
|
||||
scalar_sizes :: $enummap.init_default(Type, usize, usize(0), {
|
||||
float = 8, function = 8, pointer = 8,
|
||||
string = 16,
|
||||
bool = 1,
|
||||
void = 0,
|
||||
})
|
||||
|
||||
storage_alignment proc(module @Module, type Type, shape ArrayId) usize {
|
||||
if (aggregate(type)) return module.arrays.items[usize(shape)].alignment
|
||||
bits :: integer_bits(type)
|
||||
if (bits != 0) return divtrunc!(bits, 8)
|
||||
if (enummap.get(&scalar_alignments, type)) |alignment| return alignment
|
||||
return 1
|
||||
}
|
||||
|
||||
storage_size proc(module @Module, type Type, shape ArrayId) usize {
|
||||
if (aggregate(type)) return module.arrays.items[usize(shape)].byte_size
|
||||
bits :: integer_bits(type)
|
||||
if (bits != 0) return divtrunc!(bits, 8)
|
||||
return enummap.get(&scalar_sizes, type)?
|
||||
}
|
||||
|
||||
#! loads a scalar or borrows an aggregate from byte cells without allocating.
|
||||
read_storage proc(module @Module, type Type, shape ArrayId, cells []mut Value, raw bool) Value {
|
||||
if (aggregate(type)) return Value{ type = type, array_type = shape, raw = raw, data = ValueData{ elements = cells[..storage_size(module, type, shape)] } }
|
||||
bits u64 := 0
|
||||
size :: storage_size(module, type, shape)
|
||||
count :: if (size > 8) usize(8) else size
|
||||
for (0..count) |i| {
|
||||
if (cells[i].uninitialized) return Value{ type = type, uninitialized = true }
|
||||
}
|
||||
for (0..count) |i| bits += cells[i].data.unsigned << u64(i * 8)
|
||||
data := ValueData{ unsigned = bits }
|
||||
match type {
|
||||
.i8: data = ValueData{ integer = i64(ptrcast!(@i8, &bits)^) }
|
||||
.i16: data = ValueData{ integer = i64(ptrcast!(@i16, &bits)^) }
|
||||
.i32: data = ValueData{ integer = i64(ptrcast!(@i32, &bits)^) }
|
||||
.int, .isize: data = ValueData{ integer = ptrcast!(@i64, &bits)^ }
|
||||
.float: data = ValueData{ real = ptrcast!(@f64, &bits)^ }
|
||||
.bool: data = ValueData{ boolean = (bits & 1) != 0 }
|
||||
.string: data = ValueData{ string = StringId(bits) }
|
||||
.function: data = ValueData{ unsigned = bits }
|
||||
.void: return Value{}
|
||||
else: {}
|
||||
}
|
||||
return Value{ type = type, data = data }
|
||||
}
|
||||
|
||||
#! stores a value in shared byte cells; recursive aggregates preserve their native field padding.
|
||||
write_storage proc(module @Module, cells []mut Value, value Value) void {
|
||||
if (aggregate(value.type)) {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
if (value.raw or shape.is_union) {
|
||||
memcopy!(cells[..shape.byte_size], value.data.elements)
|
||||
return
|
||||
}
|
||||
count :: if (shape.kind == .record) shape.fields.len else shape.len
|
||||
for (0..count) |i| {
|
||||
offset :: if (shape.kind == .record) shape.fields[i].byte_offset else i * storage_size(module, shape.element, shape.element_array)
|
||||
write_storage(module, cells[offset..], aggregate_item(module, value, i))
|
||||
}
|
||||
return
|
||||
}
|
||||
bits u64 := 0
|
||||
match value.type {
|
||||
.float: bits = ptrcast!(@u64, &value.data.real)^
|
||||
.bool: bits = if (value.data.boolean) 1 else 0
|
||||
.string: bits = u64(value.data.string)
|
||||
.function: bits = if (value.data.function == maxval!(FunctionId)) maxval!(u64) else u64(value.data.function)
|
||||
.pointer: bits = value.data.unsigned
|
||||
.void: return
|
||||
else: bits = ptrcast!(@u64, &value.data.integer)^
|
||||
}
|
||||
size :: storage_size(module, value.type, value.array_type)
|
||||
for (0..size) |i| {
|
||||
byte u64 := 0
|
||||
if (i < 8) byte = (bits >> u64(i * 8)) & 255
|
||||
else byte = (u64(string_bytes(module, value.data.string).len) >> u64((i - 8) * 8)) & 255
|
||||
cells[i] = Value{ type = .u8, uninitialized = value.uninitialized, data = ValueData{ unsigned = byte } }
|
||||
}
|
||||
}
|
||||
|
||||
#! converts raw aggregate projections into the caller's preallocated flattened segment.
|
||||
copy_aggregate proc(module @Module, destination Value, source Value) void {
|
||||
shape :: module.arrays.items[usize(source.array_type)]
|
||||
if (!source.raw or shape.is_union) {
|
||||
memcopy!(destination.data.elements, source.data.elements)
|
||||
return
|
||||
}
|
||||
count :: if (shape.kind == .record) shape.fields.len else shape.len
|
||||
for (0..count) |i| {
|
||||
child :: aggregate_item(module, source, i)
|
||||
if (aggregate(child.type)) copy_aggregate(module, aggregate_item(module, destination, i), child)
|
||||
else {
|
||||
offset :: if (shape.kind == .record) shape.fields[i].offset else i
|
||||
destination.data.elements[offset] = child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
union_tag proc(module @Module, value Value) Value {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
return read_storage(module, shape.tag_type, no_array, value.data.elements, true)
|
||||
}
|
||||
|
||||
union_active proc(module @Module, value Value, field usize) bool {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
if (shape.tag_type == .void) return true
|
||||
tag :: union_tag(module, value)
|
||||
return tag.data.unsigned == shape.fields[field].tag.data.unsigned
|
||||
}
|
||||
|
||||
@hide:file
|
||||
prepare_storage proc(module @Module, function @mut Function, index u32, shape ArrayId, local bool) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
cells :: module.arrays.items[usize(shape)].cells
|
||||
if (cells > divtrunc!(maxval!(usize), sizeof!(Value)) - function.aggregate_cells) return .storage_limit_reached
|
||||
try arraylist.append(&function.array_storage, ArrayStorage{
|
||||
index = index,
|
||||
array_type = shape,
|
||||
offset = function.aggregate_cells,
|
||||
local = local,
|
||||
})
|
||||
function.aggregate_cells += cells
|
||||
}
|
||||
|
||||
#! creates a local and its disjoint by-value backing together; scalar locals use no_array.
|
||||
add_local proc(
|
||||
module @mut Module,
|
||||
function FunctionId,
|
||||
type Type,
|
||||
shape ArrayId,
|
||||
) LocalId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
debug.assert(aggregate(type) == (shape != no_array))
|
||||
body :: &module.functions.items[usize(function)]
|
||||
if (body.locals.items.len >= usize(maxval!(LocalId))) return .storage_limit_reached
|
||||
id :: LocalId(body.locals.items.len)
|
||||
try arraylist.append(&body.locals, type)
|
||||
errdefer _ = arraylist.pop(&body.locals)
|
||||
if (aggregate(type)) try prepare_storage(module, body, u32(id), shape, true)
|
||||
return id
|
||||
}
|
||||
|
||||
#! creates a parameter and its local backing; all parameters precede non-parameter locals.
|
||||
add_parameter proc(
|
||||
module @mut Module,
|
||||
function FunctionId,
|
||||
type Type,
|
||||
shape ArrayId,
|
||||
) LocalId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
body :: &module.functions.items[usize(function)]
|
||||
debug.assert(body.parameters.items.len == body.locals.items.len)
|
||||
try arraylist.append(&body.parameters, type)
|
||||
errdefer _ = arraylist.pop(&body.parameters)
|
||||
return try add_local(module, function, type, shape)
|
||||
}
|
||||
|
||||
#! appends an instruction and derives its result count, aggregate backing, and address presence.
|
||||
#! no_value requests a fresh result; explicit result IDs support non-SSA definitions across blocks.
|
||||
#! repeated aggregate results must retain their shape and reuse their existing segment.
|
||||
#! failures leave instruction and storage lengths, totals, counts, and flags unchanged.
|
||||
emit proc(
|
||||
module @mut Module,
|
||||
function FunctionId,
|
||||
block BlockId,
|
||||
instruction Instruction,
|
||||
) ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
body :: &module.functions.items[usize(function)]
|
||||
target :: &body.blocks.items[usize(block)]
|
||||
operation := instruction
|
||||
produces_value :: (
|
||||
operation.op != .store_local
|
||||
and operation.op != .assign_local
|
||||
and operation.op != .reset_local
|
||||
and operation.op != .store_address
|
||||
and operation.op != .slice_check
|
||||
and operation.op != .sentinel_check
|
||||
and operation.op != .optional_check
|
||||
and operation.op != .memory_copy
|
||||
and operation.op != .memory_set
|
||||
)
|
||||
count := body.value_count
|
||||
if (produces_value) {
|
||||
if (operation.destination == no_value) {
|
||||
if (count >= usize(no_value)) return .storage_limit_reached
|
||||
operation.destination = ValueId(count)
|
||||
}
|
||||
if (usize(operation.destination) >= count) count = usize(operation.destination) + 1
|
||||
} else {
|
||||
debug.assert(operation.destination == no_value)
|
||||
}
|
||||
try arraylist.append(&target.instructions, operation)
|
||||
errdefer _ = arraylist.pop(&target.instructions)
|
||||
if (produces_value and aggregate(operation.type)) {
|
||||
debug.assert(operation.array_type != no_array)
|
||||
registered := false
|
||||
if (usize(operation.destination) < body.value_count) {
|
||||
for (body.array_storage.items) |storage| {
|
||||
if (storage.local or storage.index != u32(operation.destination)) continue
|
||||
debug.assert(storage.array_type == operation.array_type)
|
||||
registered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!registered) try prepare_storage(module, body, u32(operation.destination), operation.array_type, false)
|
||||
}
|
||||
body.value_count = count
|
||||
if (operation.type == .address) body.has_addresses = true
|
||||
return operation.destination
|
||||
}
|
||||
|
||||
#! resolves a module-local ID to bytes borrowed until module deinit.
|
||||
string_bytes proc(module @Module, id StringId) []u8 {
|
||||
string :: module.strings.items[usize(id)]
|
||||
@@ -208,10 +650,14 @@ deinit proc(module @mut Module) void {
|
||||
arraylist.deinit(&function.locals)
|
||||
arraylist.deinit(&function.blocks)
|
||||
arraylist.deinit(&function.array_storage)
|
||||
if (function.external_name.len != 0) mem.free(module.allocator, function.external_name)
|
||||
}
|
||||
for (module.strings.items) |string|
|
||||
mem.free(module.allocator, string.allocation)
|
||||
|
||||
for (module.arrays.items) |shape| {
|
||||
if (shape.kind == .record) mem.free(module.allocator, shape.fields)
|
||||
}
|
||||
arraylist.deinit(&module.functions)
|
||||
arraylist.deinit(&module.globals)
|
||||
arraylist.deinit(&module.strings)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import "@std/mem"
|
||||
import "@std/arraylist"
|
||||
import "@std/testing"
|
||||
|
||||
shape_and_segment_limits_preserve_zero_leaf_arrays_and_allow_retry test {
|
||||
module := init(mem.c_allocator)
|
||||
defer deinit(&module)
|
||||
limit :: divtrunc!(maxval!(usize), sizeof!(Value))
|
||||
rejected := false
|
||||
_ = add_array(&module, .int, no_array, limit + 1, false) catch |error| {
|
||||
rejected = error == .storage_limit_reached
|
||||
yield no_array
|
||||
}
|
||||
try testing.expect(rejected)
|
||||
pair :: try add_array(&module, .int, no_array, 2, false)
|
||||
rejected = false
|
||||
_ = add_array(&module, .array, pair, maxval!(usize), false) catch |error| {
|
||||
rejected = error == .storage_limit_reached
|
||||
yield no_array
|
||||
}
|
||||
try testing.expect(rejected)
|
||||
full :: try add_array(&module, .int, no_array, limit - 2, false)
|
||||
empty :: try add_array(&module, .bool, no_array, 0, false)
|
||||
zero_rows :: try add_array(&module, .array, empty, maxval!(usize), false)
|
||||
try arraylist.append(&module.functions, init_function(module.allocator))
|
||||
function :: FunctionId(0)
|
||||
try arraylist.append(&module.functions.items[0].blocks, init_block(module.allocator))
|
||||
try testing.expect_equal(LocalId(0), try add_parameter(&module, function, .array, full))
|
||||
element :: try emit(&module, function, BlockId(0), Instruction{
|
||||
op = .constant, type = .int, constant = Value{ type = .int, data = ValueData{ integer = 42 } },
|
||||
})
|
||||
try arraylist.append(&module.functions.items[0].blocks.items[0].arguments, element)
|
||||
try arraylist.append(&module.functions.items[0].blocks.items[0].arguments, element)
|
||||
result :: try emit(&module, function, BlockId(0), Instruction{
|
||||
op = .array_literal, type = .array, array_type = pair, arguments_count = 2,
|
||||
})
|
||||
# redefining an array result reuses backing even when no further cells fit.
|
||||
_ = try emit(&module, function, BlockId(0), Instruction{
|
||||
op = .array_literal, type = .array, array_type = pair, arguments_count = 2, destination = result,
|
||||
})
|
||||
rejected = false
|
||||
_ = add_parameter(&module, function, .array, pair) catch |error| {
|
||||
rejected = error == .storage_limit_reached
|
||||
yield LocalId(0)
|
||||
}
|
||||
try testing.expect(rejected)
|
||||
# a rejected parameter consumes neither a parameter/local identity nor aggregate storage.
|
||||
try testing.expect_equal(LocalId(1), try add_parameter(&module, function, .array, zero_rows))
|
||||
rejected = false
|
||||
_ = emit(&module, function, BlockId(0), Instruction{
|
||||
op = .array_literal, type = .array, array_type = pair, arguments_count = 2,
|
||||
}) catch |error| {
|
||||
rejected = error == .storage_limit_reached
|
||||
yield no_value
|
||||
}
|
||||
try testing.expect(rejected)
|
||||
# a rejected result consumes no temporary; zero-leaf shapes still fit a full segment layout.
|
||||
try testing.expect_equal(ValueId(2), try emit(&module, function, BlockId(0), Instruction{
|
||||
op = .load_local, type = .array, array_type = zero_rows, slot = 1,
|
||||
}))
|
||||
}
|
||||
+180
-69
@@ -1,6 +1,7 @@
|
||||
import "@std/mem"
|
||||
import "@std/strmap"
|
||||
import "@std/arraylist"
|
||||
import "@std/enummap"
|
||||
|
||||
import "@source/diagnostics"
|
||||
import "@source/strpool"
|
||||
@@ -15,30 +16,53 @@ ScanError :: struct {
|
||||
ErrorCode :: enum {
|
||||
float_must_end_with_digit,
|
||||
unterminated_string,
|
||||
unterminated_character,
|
||||
}
|
||||
SourceError :: enum { source_too_large }
|
||||
|
||||
@hide:file
|
||||
keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
|
||||
{ "proc", .proc },
|
||||
{ "c_func", .c_func },
|
||||
{ "null", .null },
|
||||
{ "undefined", .undefined },
|
||||
{ "return", .return },
|
||||
{ "yield", .yield },
|
||||
{ "break", .break },
|
||||
{ "continue", .continue },
|
||||
{ "unreachable", .unreachable },
|
||||
{ "try", .try },
|
||||
{ "catch", .catch },
|
||||
{ "defer", .defer },
|
||||
{ "errdefer", .errdefer },
|
||||
{ "if", .if },
|
||||
{ "for", .for },
|
||||
{ "inline", .inline },
|
||||
{ "else", .else },
|
||||
{ "while", .while },
|
||||
{ "true", .true },
|
||||
{ "false", .false },
|
||||
{ "and", .and },
|
||||
{ "or", .or },
|
||||
{ "xor", .xor },
|
||||
{ "mut", .mut },
|
||||
{ "struct", .struct },
|
||||
{ "c_struct", .c_struct },
|
||||
{ "opaque", .opaque },
|
||||
{ "enum", .enum },
|
||||
{ "union", .union },
|
||||
{ "distinct", .distinct },
|
||||
{ "alias", .alias },
|
||||
{ "match", .match },
|
||||
{ "import", .import },
|
||||
{ "test", .test },
|
||||
])
|
||||
|
||||
#! owns token storage and retains its allocator; release with deinit.
|
||||
#! identifier IDs refer to strings in the separately owned strpool.strings.
|
||||
#! identifier IDs belong to the compilation's separately owned string pool.
|
||||
State :: struct {
|
||||
tokens arraylist.ArrayList(Token)
|
||||
source_id diagnostics.SourceId = diagnostics.SourceId(0)
|
||||
}
|
||||
|
||||
#! retains `allocator` for scanned tokens; the caller owns the returned State.
|
||||
@@ -46,26 +70,31 @@ init proc(allocator mem.Allocator) State {
|
||||
return State{ tokens = arraylist.init(allocator) }
|
||||
}
|
||||
|
||||
#! releases token storage with the retained allocator, without releasing strpool.strings.
|
||||
#! releases token storage with the retained allocator, without releasing identifier strings.
|
||||
deinit proc(state @mut State) void {
|
||||
arraylist.deinit(&state.tokens)
|
||||
}
|
||||
|
||||
#! borrows `program` and `diagnostic_store` during scanning; tokens remain state-owned.
|
||||
#! token allocation uses the retained allocator; identifiers use strpool.strings's allocator.
|
||||
#! initialize strpool.strings before scanning and keep it alive while using identifier IDs.
|
||||
#! borrows `program`, `pool`, and `diagnostic_store`; tokens remain state-owned.
|
||||
#! identifier storage uses the pool's allocator; keep the pool alive while using their IDs.
|
||||
#! `source_id` is copied into every token and scanner diagnostic span.
|
||||
scan proc(
|
||||
state @mut State,
|
||||
program []u8,
|
||||
pool @mut strpool.StringPool,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) void ! (mem.AllocError | strpool.InternError | diagnostics.CapacityError | SourceError) {
|
||||
if (program.len > usize(maxval!(u32))) return .source_too_large
|
||||
source :: state.source_id
|
||||
cursor := 0
|
||||
while cursor < program.len {
|
||||
char :: program[cursor]
|
||||
|
||||
if char == '\n' {
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = .newline })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .newline,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
@@ -76,32 +105,40 @@ scan proc(
|
||||
|
||||
if char == '#' {
|
||||
while (cursor < program.len and program[cursor] != '\n') cursor += 1
|
||||
if (cursor < program.len) cursor += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if is_alpha(char) or char == '_' {
|
||||
start :: cursor
|
||||
result :: scan_ident(start, program)
|
||||
result :: try scan_ident(start, program, pool)
|
||||
compound :: result.kind == .xor and result.end < program.len and program[result.end] == '='
|
||||
extra :: if (compound) usize(1) else 0
|
||||
end :: result.end + extra
|
||||
kind :: if (compound) TokenKind.xor_equal else result.kind
|
||||
try add_token(state, Token{
|
||||
span = make_span(start, result.end),
|
||||
kind = result.kind,
|
||||
span = make_span(source, start, end),
|
||||
kind = kind,
|
||||
data = TokenData{ str_id = result.str_id },
|
||||
})
|
||||
cursor = result.end
|
||||
cursor = end
|
||||
continue
|
||||
}
|
||||
|
||||
if is_digit(char) {
|
||||
start :: cursor
|
||||
result :: scan_number(start, program) catch |err| {
|
||||
member :: (
|
||||
state.tokens.items.len > 0
|
||||
and state.tokens.items[state.tokens.items.len - 1].kind == .dot
|
||||
and usize(state.tokens.items[state.tokens.items.len - 1].span.end) == start
|
||||
)
|
||||
result :: scan_number(start, program, !member) catch |err| {
|
||||
id :: try diagnostics.add(
|
||||
diagnostic_store,
|
||||
diagnostic_code(err.code),
|
||||
make_span(start, err.end),
|
||||
make_span(source, start, err.end),
|
||||
)
|
||||
try add_token(state, Token{
|
||||
span = make_span(start, err.end),
|
||||
span = make_span(source, start, err.end),
|
||||
kind = .invalid,
|
||||
data = TokenData{ diagnostic = id },
|
||||
})
|
||||
@@ -109,48 +146,64 @@ scan proc(
|
||||
continue
|
||||
}
|
||||
kind :: if (result.has_decimal) .float else .int
|
||||
try add_token(state, Token{ span = make_span(start, result.end), kind = kind })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, start, result.end),
|
||||
kind = kind,
|
||||
})
|
||||
cursor = result.end
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '"' {
|
||||
if (char == '"' or char == '\'') {
|
||||
start :: cursor
|
||||
result :: scan_string(start, program) catch |err| {
|
||||
result :: scan_quoted(start, program, char) catch |err| {
|
||||
id :: try diagnostics.add(
|
||||
diagnostic_store,
|
||||
diagnostic_code(err.code),
|
||||
make_span(start, err.end),
|
||||
make_span(source, start, err.end),
|
||||
)
|
||||
try add_token(state, Token{
|
||||
span = make_span(start, err.end),
|
||||
span = make_span(source, start, err.end),
|
||||
kind = .invalid,
|
||||
data = TokenData{ diagnostic = id },
|
||||
})
|
||||
cursor = err.end
|
||||
continue
|
||||
}
|
||||
try add_token(state, Token{ span = make_span(start, result.end), kind = .string })
|
||||
kind :: if (char == '\'') TokenKind.character else .string
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, start, result.end),
|
||||
kind = kind,
|
||||
})
|
||||
cursor = result.end
|
||||
continue
|
||||
}
|
||||
|
||||
if (char == '.' and cursor + 1 < program.len and program[cursor + 1] == '.') {
|
||||
inclusive :: cursor + 2 < program.len and program[cursor + 2] == '='
|
||||
width :: if (inclusive) 3 else 2
|
||||
range_kind :: if (inclusive) .range_inclusive else .range
|
||||
variadic :: cursor + 2 < program.len and program[cursor + 2] == '.'
|
||||
width :: if (inclusive or variadic) 3 else 2
|
||||
range_kind :: if (inclusive) .range_inclusive else if (variadic) .ellipsis else .range
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + width),
|
||||
span = make_span(source, cursor, cursor + width),
|
||||
kind = range_kind,
|
||||
})
|
||||
cursor += width
|
||||
continue
|
||||
}
|
||||
if (char == '.') {
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .dot,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if char == ':' {
|
||||
if cursor + 1 < program.len and program[cursor + 1] == ':' {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = .double_colon,
|
||||
})
|
||||
cursor += 2
|
||||
@@ -158,13 +211,16 @@ scan proc(
|
||||
}
|
||||
if cursor + 1 < program.len and program[cursor + 1] == '=' {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = .colon_equal,
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = .colon })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .colon,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
@@ -172,69 +228,106 @@ scan proc(
|
||||
if char == '+' {
|
||||
if cursor + 1 < program.len and program[cursor + 1] == '=' {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = .plus_equal,
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = .plus })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .plus,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
if char == '-' {
|
||||
if cursor + 1 < program.len and program[cursor + 1] == '=' {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = .minus_equal,
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = .minus })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .minus,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
if char == '*' {
|
||||
if cursor + 1 < program.len and program[cursor + 1] == '=' {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = .star_equal,
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = .star })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .star,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
if char == '/' {
|
||||
if cursor + 1 < program.len and program[cursor + 1] == '=' {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = .slash_equal,
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = .slash })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .slash,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
(char == '<' or char == '>') and cursor + 1 < program.len
|
||||
and program[cursor + 1] == char
|
||||
) {
|
||||
saturating :: char == '<' and cursor + 2 < program.len and program[cursor + 2] == '|'
|
||||
width :: if (saturating) usize(3) else 2
|
||||
compound :: cursor + width < program.len and program[cursor + width] == '='
|
||||
kind TokenKind := .shift_right
|
||||
if (saturating) {
|
||||
kind = if (compound) .shift_left_saturating_equal else .shift_left_saturating
|
||||
} else if (char == '<') {
|
||||
kind = if (compound) .shift_left_equal else .shift_left
|
||||
} else if (compound) kind = .shift_right_equal
|
||||
extra :: if (compound) usize(1) else 0
|
||||
end :: cursor + width + extra
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, end),
|
||||
kind = kind,
|
||||
})
|
||||
cursor = end
|
||||
continue
|
||||
}
|
||||
|
||||
if (cursor + 1 < program.len and program[cursor + 1] == '=') {
|
||||
comparison ?TokenKind :: match char {
|
||||
'=': .equal_equal
|
||||
'!': .not_equal
|
||||
'<': .less_equal
|
||||
'>': .greater_equal
|
||||
'&': .ampersand_equal
|
||||
'|': .pipe_equal
|
||||
else: null
|
||||
}
|
||||
if (comparison) |value| {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
span = make_span(source, cursor, cursor + 2),
|
||||
kind = value,
|
||||
})
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
@@ -243,11 +336,17 @@ scan proc(
|
||||
kind ?TokenKind :: match char {
|
||||
'=': .equal
|
||||
'!': .bang
|
||||
'~': .tilde
|
||||
'<': .less
|
||||
'>': .greater
|
||||
',': .comma
|
||||
'|': .pipe
|
||||
'@': .at
|
||||
'$': .dollar
|
||||
'&': .ampersand
|
||||
'^': .caret
|
||||
'?': .question
|
||||
';': .semicolon
|
||||
'(': .open_paren
|
||||
')': .close_paren
|
||||
'[': .open_bracket
|
||||
@@ -257,7 +356,10 @@ scan proc(
|
||||
else: null
|
||||
}
|
||||
if (kind) |value| {
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor + 1), kind = value })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = value,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
@@ -265,17 +367,20 @@ scan proc(
|
||||
id :: try diagnostics.add(
|
||||
diagnostic_store,
|
||||
.invalid_character,
|
||||
make_span(cursor, cursor + 1),
|
||||
make_span(source, cursor, cursor + 1),
|
||||
)
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 1),
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .invalid,
|
||||
data = TokenData{ diagnostic = id },
|
||||
})
|
||||
cursor += 1
|
||||
}
|
||||
|
||||
try add_token(state, Token{ span = make_span(cursor, cursor), kind = .eof })
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor),
|
||||
kind = .eof,
|
||||
})
|
||||
}
|
||||
|
||||
@hide:file
|
||||
@@ -286,25 +391,22 @@ ScanIdentResult :: struct {
|
||||
}
|
||||
|
||||
@hide:file
|
||||
scan_ident proc(start usize, program []u8) ScanIdentResult {
|
||||
scan_ident proc(
|
||||
start usize,
|
||||
program []u8,
|
||||
pool @mut strpool.StringPool,
|
||||
) ScanIdentResult ! (mem.AllocError | strpool.InternError) {
|
||||
cursor := start + 1
|
||||
while (
|
||||
cursor < program.len
|
||||
and (
|
||||
is_alpha(program[cursor])
|
||||
and (is_alpha(program[cursor])
|
||||
or is_digit(program[cursor])
|
||||
or program[cursor] == '_'
|
||||
)
|
||||
) {
|
||||
cursor += 1
|
||||
}
|
||||
or program[cursor] == '_')
|
||||
) cursor += 1
|
||||
|
||||
# don't intern keywords (already O(1) lookup via token kind)
|
||||
kind :: strmap.get(&keywords, program[start..cursor]) orelse .ident
|
||||
str_id :: if (kind == .ident)
|
||||
strpool.intern(&strpool.strings, program[start..cursor]) catch strpool.no_str
|
||||
else
|
||||
strpool.no_str
|
||||
# member contexts accept keywords and therefore need their interned spelling too.
|
||||
str_id :: try strpool.intern(pool, program[start..cursor])
|
||||
return ScanIdentResult{ end = cursor, kind = kind, str_id = str_id }
|
||||
}
|
||||
|
||||
@@ -315,12 +417,12 @@ ScanNumResult :: struct {
|
||||
}
|
||||
|
||||
@hide:file
|
||||
scan_number proc(start usize, program []u8) ScanNumResult ! ScanError {
|
||||
scan_number proc(start usize, program []u8, allow_decimal bool) ScanNumResult ! ScanError {
|
||||
cursor := start
|
||||
has_decimal := false
|
||||
while (cursor < program.len and is_digit(program[cursor])) cursor += 1
|
||||
if (
|
||||
cursor < program.len and program[cursor] == '.'
|
||||
allow_decimal and cursor < program.len and program[cursor] == '.'
|
||||
and !(cursor + 1 < program.len and program[cursor + 1] == '.')
|
||||
) {
|
||||
has_decimal = true
|
||||
@@ -340,19 +442,20 @@ scan_number proc(start usize, program []u8) ScanNumResult ! ScanError {
|
||||
ScanStrResult :: struct { end usize }
|
||||
|
||||
@hide:file
|
||||
scan_string proc(start usize, program []u8) ScanStrResult ! ScanError {
|
||||
cursor := start + 1 # skip first `"`
|
||||
while cursor < program.len and program[cursor] != '"' and program[cursor] != '\n' : cursor += 1 {
|
||||
# escaped quotes do not terminate the string.
|
||||
if (program[cursor] == '\\' and cursor + 1 < program.len) cursor += 1
|
||||
scan_quoted proc(start usize, program []u8, quote u8) ScanStrResult ! ScanError {
|
||||
cursor := start + 1
|
||||
while (cursor < program.len and program[cursor] != quote and program[cursor] != '\n') : cursor += 1 {
|
||||
# an escaped delimiter is content, but a newline still ends an invalid literal.
|
||||
if (program[cursor] == '\\' and cursor + 1 < program.len and program[cursor + 1] != '\n') cursor += 1
|
||||
}
|
||||
if (cursor >= program.len or program[cursor] != '"') {
|
||||
if (cursor >= program.len or program[cursor] != quote) {
|
||||
code :: if (quote == '\'') ErrorCode.unterminated_character else .unterminated_string
|
||||
return ScanError{
|
||||
code = .unterminated_string,
|
||||
code = code,
|
||||
end = cursor,
|
||||
}
|
||||
}
|
||||
return ScanStrResult{ end = cursor + 1 } # skip last `"`
|
||||
return ScanStrResult{ end = cursor + 1 }
|
||||
}
|
||||
|
||||
@hide:file
|
||||
@@ -376,17 +479,25 @@ is_digit proc(char u8) bool {
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
diagnostic_codes :: $enummap.init(ErrorCode, diagnostics.Code, {
|
||||
float_must_end_with_digit = .float_must_end_with_digit,
|
||||
unterminated_string = .unterminated_string,
|
||||
unterminated_character = .unterminated_character,
|
||||
})
|
||||
|
||||
@hide:file
|
||||
diagnostic_code proc(code ErrorCode) diagnostics.Code {
|
||||
return match code {
|
||||
.float_must_end_with_digit: .float_must_end_with_digit
|
||||
.unterminated_string: .unterminated_string
|
||||
}
|
||||
return enummap.get(&diagnostic_codes, code)?
|
||||
}
|
||||
|
||||
@hide:file
|
||||
make_span proc(start, end usize) diagnostics.Span {
|
||||
return diagnostics.Span{ start = u32(start), end = u32(end) }
|
||||
make_span proc(source diagnostics.SourceId, start, end usize) diagnostics.Span {
|
||||
return diagnostics.Span{
|
||||
start = u32(start),
|
||||
end = u32(end),
|
||||
source = source,
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
|
||||
+237
-36
@@ -3,43 +3,91 @@ import "@std/testing"
|
||||
import "@source/diagnostics"
|
||||
import "@source/strpool"
|
||||
|
||||
scans_character_literals_and_preserves_unterminated_line_provenance test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
source :: "'a' '\\n' '\\'' '\\\\' 'é' '\\\nnext :: 1"
|
||||
state.source_id = diagnostics.SourceId(7)
|
||||
try scan(&state, source, &pool, &diagnostic_store)
|
||||
expected [11]TokenKind :: [
|
||||
.character, .character, .character, .character, .character,
|
||||
.invalid, .newline, .ident, .double_colon, .int, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||
invalid :: state.tokens.items[5]
|
||||
diagnostic :: diagnostics.get(&diagnostic_store, invalid.data.diagnostic)?
|
||||
try testing.expect_equal(diagnostics.Code.unterminated_character, diagnostic.code)
|
||||
try testing.expect_equal(diagnostics.SourceId(7), diagnostic.span.source)
|
||||
try testing.expect_equal(invalid.span.start, diagnostic.span.start)
|
||||
try testing.expect_equal(invalid.span.end, diagnostic.span.end)
|
||||
try testing.expect_equal(invalid.span.end, state.tokens.items[6].span.start)
|
||||
}
|
||||
|
||||
reports_invalid_input_and_keeps_scanning test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
|
||||
try scan(&state, "if name # comment\n$1. 2 3.5 \"hi\"", &diagnostic_store)
|
||||
try scan(&state, "if name # comment\n%1. 2 3.5 \"hi\"", &pool, &diagnostic_store)
|
||||
|
||||
try testing.expect_equal(8, state.tokens.items.len)
|
||||
try testing.expect_equal(9, state.tokens.items.len)
|
||||
try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind)
|
||||
try testing.expect_equal(TokenKind.ident, state.tokens.items[1].kind)
|
||||
try testing.expect_equal(TokenKind.invalid, state.tokens.items[2].kind)
|
||||
try testing.expect_equal(TokenKind.newline, state.tokens.items[2].kind)
|
||||
try testing.expect_equal(TokenKind.invalid, state.tokens.items[3].kind)
|
||||
try testing.expect_equal(TokenKind.int, state.tokens.items[4].kind)
|
||||
try testing.expect_equal(TokenKind.float, state.tokens.items[5].kind)
|
||||
try testing.expect_equal(TokenKind.string, state.tokens.items[6].kind)
|
||||
try testing.expect_equal(TokenKind.eof, state.tokens.items[7].kind)
|
||||
try testing.expect_equal(TokenKind.invalid, state.tokens.items[4].kind)
|
||||
try testing.expect_equal(TokenKind.int, state.tokens.items[5].kind)
|
||||
try testing.expect_equal(TokenKind.float, state.tokens.items[6].kind)
|
||||
try testing.expect_equal(TokenKind.string, state.tokens.items[7].kind)
|
||||
try testing.expect_equal(TokenKind.eof, state.tokens.items[8].kind)
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
try testing.expect_equal(
|
||||
"name",
|
||||
strpool.get_str(&strpool.strings, state.tokens.items[1].data.str_id)?,
|
||||
strpool.get_str(&pool, state.tokens.items[1].data.str_id)?,
|
||||
)
|
||||
try testing.expect_equal(u32(3), state.tokens.items[1].span.start)
|
||||
try testing.expect_equal(u32(7), state.tokens.items[1].span.end)
|
||||
}
|
||||
|
||||
scans_compile_time_parameter_markers test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
try scan(&state, "$T $K, $V type $N usize", &pool, &diagnostic_store)
|
||||
expected [12]TokenKind :: [
|
||||
.dollar, .ident, .dollar, .ident, .comma, .dollar, .ident, .ident,
|
||||
.dollar, .ident, .ident, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
|
||||
try testing.expect_equal(u32(1), state.tokens.items[0].span.end)
|
||||
try testing.expect_equal(u32(15), state.tokens.items[8].span.start)
|
||||
try testing.expect_equal(u32(16), state.tokens.items[8].span.end)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
reserves_boolean_keywords_and_scans_comparisons test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
source :: "true false bool trueish == != < <= > >="
|
||||
try scan(&state, source, &diagnostic_store)
|
||||
try scan(&state, source, &pool, &diagnostic_store)
|
||||
expected [11]TokenKind :: [
|
||||
TokenKind.true, .false, .ident, .ident,
|
||||
.equal_equal, .not_equal, .less, .less_equal, .greater, .greater_equal, .eof,
|
||||
@@ -50,29 +98,29 @@ reserves_boolean_keywords_and_scans_comparisons test {
|
||||
try testing.expect_equal(u32(4), state.tokens.items[0].span.end)
|
||||
try testing.expect_equal(u32(5), state.tokens.items[1].span.start)
|
||||
try testing.expect_equal(u32(10), state.tokens.items[1].span.end)
|
||||
try testing.expect_equal("bool", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal("bool", strpool.get_str(&pool, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
scans_logical_operators_without_splitting_identifiers test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
source :: "and or android orbit and_1 or2 ! != !! !true"
|
||||
try scan(&state, source, &diagnostic_store)
|
||||
try scan(&state, source, &pool, &diagnostic_store)
|
||||
expected [13]TokenKind :: [
|
||||
TokenKind.and, .or, .ident, .ident, .ident, .ident,
|
||||
.bang, .not_equal, .bang, .bang, .bang, .true, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal("android", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal("orbit", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?)
|
||||
try testing.expect_equal("and_1", strpool.get_str(&strpool.strings, state.tokens.items[4].data.str_id)?)
|
||||
try testing.expect_equal("or2", strpool.get_str(&strpool.strings, state.tokens.items[5].data.str_id)?)
|
||||
try testing.expect_equal("android", strpool.get_str(&pool, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal("orbit", strpool.get_str(&pool, state.tokens.items[3].data.str_id)?)
|
||||
try testing.expect_equal("and_1", strpool.get_str(&pool, state.tokens.items[4].data.str_id)?)
|
||||
try testing.expect_equal("or2", strpool.get_str(&pool, state.tokens.items[5].data.str_id)?)
|
||||
try testing.expect_equal(u32(31), state.tokens.items[6].span.start)
|
||||
try testing.expect_equal(u32(32), state.tokens.items[6].span.end)
|
||||
try testing.expect_equal(u32(33), state.tokens.items[7].span.start)
|
||||
@@ -81,14 +129,14 @@ scans_logical_operators_without_splitting_identifiers test {
|
||||
}
|
||||
|
||||
scans_loop_controls_with_exact_keyword_boundaries test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
source :: "break continue breakfast continued break_1 continue2"
|
||||
try scan(&state, source, &diagnostic_store)
|
||||
try scan(&state, source, &pool, &diagnostic_store)
|
||||
expected [7]TokenKind :: [.break, .continue, .ident, .ident, .ident, .ident, .eof]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
@@ -96,23 +144,42 @@ scans_loop_controls_with_exact_keyword_boundaries test {
|
||||
try testing.expect_equal(u32(5), state.tokens.items[0].span.end)
|
||||
try testing.expect_equal(u32(6), state.tokens.items[1].span.start)
|
||||
try testing.expect_equal(u32(14), state.tokens.items[1].span.end)
|
||||
try testing.expect_equal("breakfast", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal("continued", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?)
|
||||
try testing.expect_equal("breakfast", strpool.get_str(&pool, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal("continued", strpool.get_str(&pool, state.tokens.items[3].data.str_id)?)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
scans_ranges_without_consuming_float_dots test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
scans_inline_keyword_without_splitting_identifiers test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
try scan(&state, "0..10 0..=10 1.5..2.5 |@item, _| ... 1.", &diagnostic_store)
|
||||
expected [19]TokenKind :: [
|
||||
try scan(&state, "inline for inlined inline_1 inline2", &pool, &diagnostic_store)
|
||||
expected [6]TokenKind :: [.inline, .for, .ident, .ident, .ident, .eof]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
|
||||
try testing.expect_equal(u32(6), state.tokens.items[0].span.end)
|
||||
try testing.expect_equal("inlined", strpool.get_str(&pool, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal("inline_1", strpool.get_str(&pool, state.tokens.items[3].data.str_id)?)
|
||||
try testing.expect_equal("inline2", strpool.get_str(&pool, state.tokens.items[4].data.str_id)?)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
scans_ranges_without_consuming_float_dots test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
try scan(&state, "0..10 0..=10 1.5..2.5 |@item, _| ... 1.", &pool, &diagnostic_store)
|
||||
expected [18]TokenKind :: [
|
||||
.int, .range, .int, .int, .range_inclusive, .int,
|
||||
.float, .range, .float, .pipe, .at, .ident, .comma, .ident, .pipe,
|
||||
.range, .invalid, .invalid, .eof,
|
||||
.ellipsis, .invalid, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
@@ -120,17 +187,17 @@ scans_ranges_without_consuming_float_dots test {
|
||||
try testing.expect_equal(u32(3), state.tokens.items[1].span.end)
|
||||
try testing.expect_equal(u32(7), state.tokens.items[4].span.start)
|
||||
try testing.expect_equal(u32(10), state.tokens.items[4].span.end)
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
scans_array_delimiters_and_mut_keyword test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
try scan(&state, "[_]mut [2]i64 [1,2] [] mutable mut_1", &diagnostic_store)
|
||||
try scan(&state, "[_]mut [2]i64 [1,2] [] mutable mut_1", &pool, &diagnostic_store)
|
||||
expected [18]TokenKind :: [
|
||||
.open_bracket, .ident, .close_bracket, .mut,
|
||||
.open_bracket, .int, .close_bracket, .ident,
|
||||
@@ -141,3 +208,137 @@ scans_array_delimiters_and_mut_keyword test {
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
reuses_identifier_spellings test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
try scan(&state, "alpha beta alpha if beta", &pool, &diagnostic_store)
|
||||
first :: state.tokens.items[0].data.str_id
|
||||
second :: state.tokens.items[1].data.str_id
|
||||
try testing.expect(first != second)
|
||||
try testing.expect_equal(first, state.tokens.items[2].data.str_id)
|
||||
try testing.expect_equal(second, state.tokens.items[4].data.str_id)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
AllocationState :: struct {
|
||||
remaining usize
|
||||
live usize = 0
|
||||
}
|
||||
|
||||
@hide:file
|
||||
limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
|
||||
state @mut AllocationState :: ptrcast!(context?)
|
||||
if (state.remaining == 0) return null
|
||||
state.remaining -= 1
|
||||
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
|
||||
if (memory) |_| state.live += 1
|
||||
return memory
|
||||
}
|
||||
|
||||
@hide:file
|
||||
limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
|
||||
state @mut AllocationState :: ptrcast!(context?)
|
||||
if (state.remaining == 0) return null
|
||||
state.remaining -= 1
|
||||
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
|
||||
if (resized) |_| {
|
||||
if (memory) |_| {} else state.live += 1
|
||||
}
|
||||
return resized
|
||||
}
|
||||
|
||||
@hide:file
|
||||
limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
|
||||
state @mut AllocationState :: ptrcast!(context?)
|
||||
if (memory) |_| state.live -= 1
|
||||
mem.raw_free(mem.c_allocator, memory, size, alignment)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
|
||||
|
||||
propagates_identifier_interning_allocation_failure test {
|
||||
for (usize(0)..usize(3)) |remaining| {
|
||||
allocation := AllocationState{ remaining = remaining }
|
||||
allocator :: mem.Allocator{ context = &allocation, vtable = &limited_vtable }
|
||||
pool := strpool.init(allocator)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
failed := false
|
||||
scan(&state, "identifier", &pool, &diagnostic_store) catch |error| {
|
||||
failed = error == .out_of_memory
|
||||
}
|
||||
empty :: state.tokens.items.len == 0 and pool.strings.items.len == 0
|
||||
absent :: strpool.get_id(&pool, "identifier") == null
|
||||
strpool.deinit(&pool)
|
||||
try testing.expect(failed and empty and absent)
|
||||
try testing.expect_equal(usize(0), diagnostics.count(&diagnostic_store))
|
||||
try testing.expect_equal(usize(0), allocation.live)
|
||||
}
|
||||
}
|
||||
|
||||
scans_import_keywords_and_preserves_source_in_tokens_and_diagnostics test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
state.source_id = diagnostics.SourceId(7)
|
||||
try scan(&state, "import test %\n1.\n\"unfinished", &pool, &diagnostic_store)
|
||||
try testing.expect_equal(TokenKind.import, state.tokens.items[0].kind)
|
||||
try testing.expect_equal(TokenKind.test, state.tokens.items[1].kind)
|
||||
for (state.tokens.items) |token| try testing.expect_equal(state.source_id, token.span.source)
|
||||
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
|
||||
for (diagnostic_store.list.items) |diagnostic| {
|
||||
try testing.expect_equal(state.source_id, diagnostic.span.source)
|
||||
}
|
||||
}
|
||||
|
||||
reserves_yield_without_splitting_identifiers test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
try scan(&state, "yield yielding yield_1 .yield", &pool, &diagnostic_store)
|
||||
expected [6]TokenKind :: [.yield, .ident, .ident, .dot, .yield, .eof]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal("yield", strpool.get_str(&pool, state.tokens.items[0].data.str_id)?)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
scans_bitwise_operators_and_contiguous_compounds test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
source :: "~ & | xor << >> <<| += -= *= /= &= |= xor= <<= >>= <<|= xor_flag xor = << |= <<| ="
|
||||
try scan(&state, source, &pool, &diagnostic_store)
|
||||
expected [25]TokenKind :: [
|
||||
.tilde, .ampersand, .pipe, .xor, .shift_left, .shift_right, .shift_left_saturating,
|
||||
.plus_equal, .minus_equal, .star_equal, .slash_equal,
|
||||
.ampersand_equal, .pipe_equal, .xor_equal,
|
||||
.shift_left_equal, .shift_right_equal, .shift_left_saturating_equal,
|
||||
.ident, .xor, .equal, .shift_left, .pipe_equal, .shift_left_saturating, .equal, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
xor_span :: state.tokens.items[13].span
|
||||
try testing.expect_equal("xor=", source[usize(xor_span.start)..usize(xor_span.end)])
|
||||
saturating_span :: state.tokens.items[16].span
|
||||
try testing.expect_equal("<<|=", source[usize(saturating_span.start)..usize(saturating_span.end)])
|
||||
try testing.expect_equal("xor_flag", strpool.get_str(&pool, state.tokens.items[17].data.str_id)?)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
+30
-13
@@ -20,14 +20,29 @@ TokenKind :: enum {
|
||||
# keywords
|
||||
if, else
|
||||
for, while
|
||||
match
|
||||
inline
|
||||
proc, return
|
||||
yield
|
||||
break, continue
|
||||
unreachable
|
||||
try, catch
|
||||
defer, errdefer
|
||||
true, false
|
||||
and, or
|
||||
xor
|
||||
struct, c_struct
|
||||
opaque
|
||||
enum, union
|
||||
distinct, alias
|
||||
import, test
|
||||
mut
|
||||
c_func
|
||||
null
|
||||
undefined
|
||||
|
||||
# literals
|
||||
ident, string
|
||||
ident, string, character
|
||||
int, float
|
||||
|
||||
# comparison
|
||||
@@ -43,8 +58,14 @@ TokenKind :: enum {
|
||||
plus, minus
|
||||
star, slash
|
||||
|
||||
range, range_inclusive
|
||||
pipe, at
|
||||
# bitwise
|
||||
tilde
|
||||
shift_left, shift_right, shift_left_saturating
|
||||
|
||||
range, range_inclusive, ellipsis
|
||||
at, ampersand, caret, dollar
|
||||
question, semicolon
|
||||
pipe
|
||||
|
||||
# logical
|
||||
bang
|
||||
@@ -52,13 +73,15 @@ TokenKind :: enum {
|
||||
# assignment & arithmetic
|
||||
plus_equal, minus_equal
|
||||
star_equal, slash_equal
|
||||
ampersand_equal, pipe_equal, xor_equal
|
||||
shift_left_equal, shift_right_equal, shift_left_saturating_equal
|
||||
|
||||
# delimiters
|
||||
open_paren, close_paren
|
||||
open_bracket, close_bracket
|
||||
open_curly, close_curly
|
||||
|
||||
comma
|
||||
comma, dot
|
||||
colon
|
||||
newline
|
||||
|
||||
@@ -69,16 +92,10 @@ TokenKind :: enum {
|
||||
|
||||
render_token proc(token Token, program []u8) void {
|
||||
match token.kind {
|
||||
.ident: {
|
||||
if (strpool.get_str(&strpool.strings, token.data.str_id)) |str| {
|
||||
debug.print("{}({})\n", {token.kind, str})
|
||||
} else {
|
||||
debug.print("{}({})\n", {token.kind, program[token.span.start..token.span.end]})
|
||||
}
|
||||
}
|
||||
.string, .int, .float:
|
||||
.ident, .string, .character, .int, .float:
|
||||
debug.print("{}({})\n", {token.kind, program[token.span.start..token.span.end]})
|
||||
else: debug.print("{}\n", {token.kind})
|
||||
else:
|
||||
debug.print("{}\n", {token.kind})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,761 @@
|
||||
import "@std/io"
|
||||
import "@std/enummap"
|
||||
import "@source/ir"
|
||||
import "@source/diagnostics"
|
||||
|
||||
@hide:file
|
||||
bitwise_opcodes :: $enummap.init_default(ir.Op, []u8, "", {
|
||||
bit_and = "and",
|
||||
bit_or = "or",
|
||||
bit_xor = "xor",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
float_opcodes :: $enummap.init_default(ir.Op, []u8, "", {
|
||||
add = "fadd",
|
||||
subtract = "fsub",
|
||||
multiply = "fmul",
|
||||
divide = "fdiv",
|
||||
remainder = "frem",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
integer_opcodes :: $enummap.init_default(ir.Op, []u8, "", {
|
||||
add = "add",
|
||||
subtract = "sub",
|
||||
multiply = "mul",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
float_predicates :: $enummap.init_default(ir.Op, []u8, "", {
|
||||
equal = "oeq",
|
||||
not_equal = "une",
|
||||
less = "olt",
|
||||
less_equal = "ole",
|
||||
greater = "ogt",
|
||||
greater_equal = "oge",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
integer_predicates :: $enummap.init_default(ir.Op, []u8, "", {
|
||||
equal = "eq",
|
||||
not_equal = "ne",
|
||||
less = "lt",
|
||||
less_equal = "le",
|
||||
greater = "gt",
|
||||
greater_equal = "ge",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
conversion_upper_bounds :: $enummap.init_default(ir.Type, []u8, "", {
|
||||
i8 = "1.280000e+02",
|
||||
i16 = "3.276800e+04",
|
||||
i32 = "2.147483648e+09",
|
||||
int = "9.223372036854775808e+18",
|
||||
isize = "9.223372036854775808e+18",
|
||||
u8 = "2.560000e+02",
|
||||
u16 = "6.553600e+04",
|
||||
u32 = "4.294967296e+09",
|
||||
u64 = "1.8446744073709551616e+19",
|
||||
usize = "1.8446744073709551616e+19",
|
||||
})
|
||||
|
||||
@hide
|
||||
emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruction) void ! io.WriteError {
|
||||
output :: state.output
|
||||
type :: Type{ kind = instruction.type, shape = instruction.array_type }
|
||||
result := fresh(state)
|
||||
match instruction.op {
|
||||
.constant: {
|
||||
if (instruction.type == .void) return
|
||||
try io.write_all(output, " store ")
|
||||
try write_type(output, state.module, type)
|
||||
try io.write_all(output, " ")
|
||||
try write_constant(output, state.module, instruction.constant)
|
||||
try io.print(", ptr %v{}\n", output, {u32(instruction.destination)})
|
||||
return
|
||||
}
|
||||
.undefined_value: {
|
||||
try io.write_all(output, " store ")
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(" poison, ptr %v{}\n", output, {u32(instruction.destination)})
|
||||
return
|
||||
}
|
||||
.load_local: {
|
||||
try check_local(state, instruction.slot, true, instruction.span)
|
||||
try io.print(" %r{} = load ", output, {result})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(", ptr %l{}\n", output, {instruction.slot})
|
||||
}
|
||||
.store_local, .assign_local: {
|
||||
if (instruction.op == .assign_local) try check_local(state, instruction.slot, false, instruction.span)
|
||||
value :: try load(state, instruction.left)
|
||||
try io.write_all(output, " store ")
|
||||
try write_type(output, state.module, state.values[usize(instruction.left)])
|
||||
try io.print(" %r{}, ptr %l{}\n", output, {value, instruction.slot})
|
||||
if (instruction.op == .store_local) try io.print(" store i1 true, ptr %i{}\n", output, {instruction.slot})
|
||||
return
|
||||
}
|
||||
.reset_local: {
|
||||
try io.print(" store i1 false, ptr %i{}\n", output, {instruction.slot})
|
||||
return
|
||||
}
|
||||
.load_global: {
|
||||
pointer :: fresh(state)
|
||||
position :: location(state.line_starts, instruction.span)
|
||||
try io.print(" %r{} = call ptr @global{}(i64 {}, i64 {}, i64 {})\n", output, {pointer, instruction.slot, usize(instruction.span.source), position.line, position.column})
|
||||
try io.print(" %r{} = load ", output, {result})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(", ptr %r{}\n", output, {pointer})
|
||||
}
|
||||
.array_literal, .record_literal: {
|
||||
for (0..instruction.arguments_count) |index| {
|
||||
operand :: block.arguments.items[instruction.arguments_start + index]
|
||||
value :: try load(state, operand)
|
||||
pointer :: fresh(state)
|
||||
try io.print(" %r{} = getelementptr ", output, {pointer})
|
||||
try write_type(output, state.module, type)
|
||||
index_type []u8 :: if (instruction.op == .record_literal) "i32" else "i64"
|
||||
try io.print(", ptr %v{}, i64 0, {s} {}\n store ", output, {u32(instruction.destination), index_type, index})
|
||||
try write_type(output, state.module, state.values[usize(operand)])
|
||||
try io.print(" %r{}, ptr %r{}\n", output, {value, pointer})
|
||||
}
|
||||
return
|
||||
}
|
||||
.union_literal: {
|
||||
shape :: state.module.arrays.items[usize(instruction.array_type)]
|
||||
try io.write_all(output, " store ")
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(" zeroinitializer, ptr %v{}\n", output, {u32(instruction.destination)})
|
||||
if (shape.tag_type != .void) {
|
||||
try io.write_all(output, " store ")
|
||||
try write_type(output, state.module, Type{ kind = shape.tag_type })
|
||||
try io.write_all(output, " ")
|
||||
try write_constant(output, state.module, instruction.constant)
|
||||
try io.print(", ptr %v{}\n", output, {u32(instruction.destination)})
|
||||
}
|
||||
if (instruction.left != ir.no_value) {
|
||||
payload :: try load(state, instruction.left)
|
||||
pointer :: fresh(state)
|
||||
try io.print(" %r{} = getelementptr i8, ptr %v{}, i64 {}\n", output, {pointer, u32(instruction.destination), shape.payload_offset})
|
||||
try store_payload(state, state.values[usize(instruction.left)], payload, pointer)
|
||||
}
|
||||
return
|
||||
}
|
||||
.union_tag: {
|
||||
try io.print(" %r{} = load ", output, {result})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(", ptr %v{}\n", output, {u32(instruction.left)})
|
||||
}
|
||||
.field: {
|
||||
base_type :: state.values[usize(instruction.left)]
|
||||
shape :: state.module.arrays.items[usize(base_type.shape)]
|
||||
if (shape.is_union) {
|
||||
pointer :: fresh(state)
|
||||
try io.print(" %r{} = getelementptr i8, ptr %v{}, i64 0\n", output, {pointer, u32(instruction.left)})
|
||||
try check_union(state, pointer, shape, usize(instruction.slot), instruction.span)
|
||||
if (instruction.type == .void) return
|
||||
payload :: fresh(state)
|
||||
try io.print(" %r{} = getelementptr i8, ptr %r{}, i64 {}\n %r{} = load ", output, {payload, pointer, shape.payload_offset, result})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(", ptr %r{}\n", output, {payload})
|
||||
} else {
|
||||
base :: try load(state, instruction.left)
|
||||
try io.print(" %r{} = extractvalue ", output, {result})
|
||||
try write_type(output, state.module, base_type)
|
||||
try io.print(" %r{}, {}\n", output, {base, instruction.slot})
|
||||
}
|
||||
}
|
||||
.field_address: {
|
||||
base_type :: state.values[usize(instruction.left)]
|
||||
base :: try load(state, instruction.left)
|
||||
data :: fresh(state)
|
||||
flag :: fresh(state)
|
||||
pointer :: fresh(state)
|
||||
address :: fresh(state)
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {data, base})
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, base})
|
||||
shape :: state.module.arrays.items[usize(base_type.shape)]
|
||||
if (shape.is_union) {
|
||||
if (shape.tag_type != .void) {
|
||||
position :: location(state.line_starts, instruction.span)
|
||||
try io.print(" call void @check_init(ptr %r{}, i1 true, i64 {}, i64 {}, i64 {})\n", output, {flag, usize(instruction.span.source), position.line, position.column})
|
||||
try check_union(state, data, shape, usize(instruction.slot), instruction.span)
|
||||
}
|
||||
try io.print(" %r{} = getelementptr i8, ptr %r{}, i64 {}\n", output, {pointer, data, shape.payload_offset})
|
||||
} else {
|
||||
try io.print(" %r{} = getelementptr ", output, {pointer})
|
||||
try write_type(output, state.module, Type{ kind = .record, shape = base_type.shape })
|
||||
try io.print(", ptr %r{}, i64 0, i32 {}\n", output, {data, instruction.slot})
|
||||
}
|
||||
try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer})
|
||||
try io.print(" %r{} = insertvalue %address %r{}, ptr %r{}, 1\n", output, {result, address, flag})
|
||||
}
|
||||
.index, .index_address: {
|
||||
index :: try load(state, instruction.right)
|
||||
base_type :: state.values[usize(instruction.left)]
|
||||
shape :: state.module.arrays.items[usize(base_type.shape)]
|
||||
failed :: fresh(state)
|
||||
try io.print(" %r{} = icmp uge i64 %r{}, {}\n", output, {failed, index, shape.len})
|
||||
try check_failure(state, failed, .index_out_of_bounds, instruction.span)
|
||||
pointer :: fresh(state)
|
||||
if (instruction.op == .index_address) {
|
||||
base :: try load(state, instruction.left)
|
||||
data :: fresh(state)
|
||||
flag :: fresh(state)
|
||||
address :: fresh(state)
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {data, base})
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, base})
|
||||
try io.print(" %r{} = getelementptr ", output, {pointer})
|
||||
try write_type(output, state.module, Type{ kind = .array, shape = base_type.shape })
|
||||
try io.print(", ptr %r{}, i64 0, i64 %r{}\n", output, {data, index})
|
||||
try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer})
|
||||
try io.print(" %r{} = insertvalue %address %r{}, ptr %r{}, 1\n", output, {result, address, flag})
|
||||
} else {
|
||||
try io.print(" %r{} = getelementptr ", output, {pointer})
|
||||
try write_type(output, state.module, base_type)
|
||||
try io.print(", ptr %v{}, i64 0, i64 %r{}\n %r{} = load ", output, {u32(instruction.left), index, result})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(", ptr %r{}\n", output, {pointer})
|
||||
}
|
||||
}
|
||||
.address_local: {
|
||||
address :: fresh(state)
|
||||
try io.print(" %r{} = insertvalue %address poison, ptr %l{}, 0\n", output, {address, instruction.slot})
|
||||
try io.print(" %r{} = insertvalue %address %r{}, ptr %i{}, 1\n", output, {result, address, instruction.slot})
|
||||
}
|
||||
.address_global: {
|
||||
pointer :: fresh(state)
|
||||
address :: fresh(state)
|
||||
position :: location(state.line_starts, instruction.span)
|
||||
try io.print(" %r{} = call ptr @global{}(i64 {}, i64 {}, i64 {})\n", output, {pointer, instruction.slot, usize(instruction.span.source), position.line, position.column})
|
||||
try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer})
|
||||
try io.print(" %r{} = insertvalue %address %r{}, ptr null, 1\n", output, {result, address})
|
||||
}
|
||||
.pointer_from_address: {
|
||||
address :: try load(state, instruction.left)
|
||||
flag :: fresh(state)
|
||||
position :: location(state.line_starts, instruction.span)
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, address})
|
||||
try io.print(" call void @check_init(ptr %r{}, i1 true, i64 {}, i64 {}, i64 {})\n", output, {flag, usize(instruction.span.source), position.line, position.column})
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {result, address})
|
||||
}
|
||||
.pointer_address: {
|
||||
pointer :: try load(state, instruction.left)
|
||||
missing :: fresh(state)
|
||||
try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {missing, pointer})
|
||||
try check_failure(state, missing, .invalid_pointer_access, instruction.span)
|
||||
address :: fresh(state)
|
||||
try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer})
|
||||
try io.print(" %r{} = insertvalue %address %r{}, ptr null, 1\n", output, {result, address})
|
||||
}
|
||||
.pointer_offset: {
|
||||
pointer :: try load(state, instruction.left)
|
||||
index :: try load(state, instruction.right)
|
||||
try io.print(" %r{} = getelementptr ", output, {result})
|
||||
try write_type(output, state.module, Type{ kind = instruction.pointee, shape = instruction.array_type })
|
||||
try io.print(", ptr %r{}, i64 %r{}\n", output, {pointer, index})
|
||||
}
|
||||
.pointer_cast: result = try load(state, instruction.left)
|
||||
.string_pointer, .string_length: {
|
||||
string :: try load(state, instruction.left)
|
||||
field usize :: if (instruction.op == .string_pointer) 0 else 1
|
||||
try io.print(" %r{} = extractvalue %string %r{}, {}\n", output, {result, string, field})
|
||||
}
|
||||
.optional_check: {
|
||||
operand :: try load(state, instruction.left)
|
||||
failed :: fresh(state)
|
||||
if (state.values[usize(instruction.left)].kind == .bool) try io.print(" %r{} = xor i1 %r{}, true\n", output, {failed, operand})
|
||||
else try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {failed, operand})
|
||||
try check_failure(state, failed, .null_unwrap, instruction.span)
|
||||
return
|
||||
}
|
||||
.slice_check: {
|
||||
start :: try load(state, instruction.left)
|
||||
end :: try load(state, instruction.right)
|
||||
reversed :: fresh(state)
|
||||
try io.print(" %r{} = icmp ugt i64 %r{}, %r{}\n", output, {reversed, start, end})
|
||||
failed := reversed
|
||||
if (instruction.extra != ir.no_value) {
|
||||
bound :: try load(state, instruction.extra)
|
||||
outside :: fresh(state)
|
||||
failed = fresh(state)
|
||||
try io.print(" %r{} = icmp ugt i64 %r{}, %r{}\n", output, {outside, end, bound})
|
||||
try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {failed, reversed, outside})
|
||||
}
|
||||
try check_failure(state, failed, .invalid_slice_bounds, instruction.span)
|
||||
return
|
||||
}
|
||||
.sentinel_check: {
|
||||
pointer :: try load(state, instruction.left)
|
||||
expected :: try load(state, instruction.right)
|
||||
missing :: fresh(state)
|
||||
try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {missing, pointer})
|
||||
try check_failure(state, missing, .invalid_pointer_access, instruction.span)
|
||||
actual :: fresh(state)
|
||||
element :: Type{ kind = instruction.pointee, shape = instruction.array_type }
|
||||
try io.print(" %r{} = load ", output, {actual})
|
||||
try write_type(output, state.module, element)
|
||||
try io.print(", ptr %r{}, align 1\n", output, {pointer})
|
||||
failed :: try sentinel_difference(state, element, actual, expected)
|
||||
try check_failure(state, failed, .sentinel_mismatch, instruction.span)
|
||||
return
|
||||
}
|
||||
.memory_copy, .memory_set: {
|
||||
try emit_memory(state, instruction)
|
||||
return
|
||||
}
|
||||
.load_address, .store_address: {
|
||||
address :: try load(state, instruction.left)
|
||||
pointer :: fresh(state)
|
||||
flag :: fresh(state)
|
||||
position :: location(state.line_starts, instruction.span)
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {pointer, address})
|
||||
try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, address})
|
||||
try io.print(" call void @check_init(ptr %r{}, i1 {}, i64 {}, i64 {}, i64 {})\n", output, {flag, instruction.op == .load_address, usize(instruction.span.source), position.line, position.column})
|
||||
if (instruction.op == .store_address) {
|
||||
value :: try load(state, instruction.right)
|
||||
try store_payload(state, state.values[usize(instruction.right)], value, pointer)
|
||||
return
|
||||
}
|
||||
try io.print(" %r{} = load ", output, {result})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(", ptr %r{}, align 1\n", output, {pointer})
|
||||
}
|
||||
.call: {
|
||||
callee :: try load(state, instruction.left)
|
||||
# operands are frame snapshots; loading them before the call preserves evaluation order.
|
||||
# argument registers are consecutive because load emits exactly one fresh register.
|
||||
first_argument :: state.next_register
|
||||
for (0..instruction.arguments_count) |index| {
|
||||
_ = try load(state, block.arguments.items[instruction.arguments_start + index])
|
||||
}
|
||||
# C default promotions apply only after the fixed prefix.
|
||||
first_promotion :: state.next_register
|
||||
if (instruction.variadic) {
|
||||
for (instruction.fixed_arguments..instruction.arguments_count) |index| {
|
||||
operand :: block.arguments.items[instruction.arguments_start + index]
|
||||
kind :: state.values[usize(operand)].kind
|
||||
if (variadic_type(kind) == kind) continue
|
||||
promoted :: fresh(state)
|
||||
opcode []u8 :: if (kind == .bool or ir.unsigned_integer(kind)) "zext" else "sext"
|
||||
try io.print(" %r{} = {s} ", output, {promoted, opcode})
|
||||
try write_type(output, state.module, state.values[usize(operand)])
|
||||
try io.print(" %r{} to i32\n", output, {first_argument + index})
|
||||
}
|
||||
}
|
||||
if (instruction.type != .void) try io.print(" %r{} = call ", output, {result})
|
||||
else try io.write_all(output, " call ")
|
||||
try write_abi_result(output, state.module, type)
|
||||
if (instruction.variadic) {
|
||||
try io.write_all(output, " (")
|
||||
for (0..instruction.fixed_arguments) |index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
operand :: block.arguments.items[instruction.arguments_start + index]
|
||||
try write_type(output, state.module, state.values[usize(operand)])
|
||||
}
|
||||
if (instruction.fixed_arguments != 0) try io.write_all(output, ", ")
|
||||
try io.write_all(output, "...)")
|
||||
}
|
||||
try io.print(" %r{}(", output, {callee})
|
||||
promotion := first_promotion
|
||||
for (0..instruction.arguments_count) |index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
operand :: block.arguments.items[instruction.arguments_start + index]
|
||||
argument_type := state.values[usize(operand)]
|
||||
register := first_argument + index
|
||||
if (instruction.variadic and index >= instruction.fixed_arguments) {
|
||||
kind :: variadic_type(argument_type.kind)
|
||||
if (kind != argument_type.kind) {
|
||||
register = promotion
|
||||
promotion += 1
|
||||
argument_type.kind = kind
|
||||
}
|
||||
}
|
||||
try write_abi_parameter(output, state.module, argument_type)
|
||||
try io.print(" %r{}", output, {register})
|
||||
}
|
||||
try io.write_all(output, ")\n")
|
||||
}
|
||||
.convert: result = try emit_conversion(state, instruction)
|
||||
.negate: {
|
||||
operand :: try load(state, instruction.left)
|
||||
if (instruction.type == .float) {
|
||||
try io.print(" %r{} = fneg double %r{}\n", output, {result, operand})
|
||||
} else {
|
||||
pair :: fresh(state)
|
||||
overflow :: fresh(state)
|
||||
bits :: ir.integer_bits(instruction.type)
|
||||
sign []u8 :: if (ir.unsigned_integer(instruction.type)) "u" else "s"
|
||||
try io.print(" %r{} = call {{ i{}, i1 }} @llvm.{s}sub.with.overflow.i{}(i{} 0, i{} %r{})\n", output, {pair, bits, sign, bits, bits, bits, operand})
|
||||
try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 1\n", output, {overflow, bits, pair})
|
||||
try check_failure(state, overflow, .integer_overflow, instruction.span)
|
||||
try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 0\n", output, {result, bits, pair})
|
||||
}
|
||||
}
|
||||
.logical_not: {
|
||||
operand :: try load(state, instruction.left)
|
||||
try io.print(" %r{} = xor i1 %r{}, true\n", output, {result, operand})
|
||||
}
|
||||
.bit_not: {
|
||||
operand :: try load(state, instruction.left)
|
||||
try io.print(" %r{} = xor i{} %r{}, -1\n", output, {result, ir.integer_bits(instruction.type), operand})
|
||||
}
|
||||
.bit_and, .bit_or, .bit_xor: {
|
||||
left :: try load(state, instruction.left)
|
||||
right :: try load(state, instruction.right)
|
||||
opcode :: enummap.get(&bitwise_opcodes, instruction.op)?
|
||||
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {result, opcode, ir.integer_bits(instruction.type), left, right})
|
||||
}
|
||||
.shift_left, .shift_right, .shift_left_saturating: result = try emit_shift(state, instruction)
|
||||
.add, .subtract, .multiply, .divide, .remainder: {
|
||||
left :: try load(state, instruction.left)
|
||||
right :: try load(state, instruction.right)
|
||||
if (instruction.type == .float) {
|
||||
if (instruction.op == .remainder) {
|
||||
zero :: fresh(state)
|
||||
try io.print(" %r{} = fcmp oeq double %r{}, 0.0\n", output, {zero, right})
|
||||
try check_failure(state, zero, .division_by_zero, instruction.span)
|
||||
}
|
||||
opcode :: enummap.get(&float_opcodes, instruction.op)?
|
||||
try io.print(" %r{} = {s} double %r{}, %r{}\n", output, {result, opcode, left, right})
|
||||
} else if (instruction.op == .divide or instruction.op == .remainder) {
|
||||
bits :: ir.integer_bits(instruction.type)
|
||||
zero :: fresh(state)
|
||||
try io.print(" %r{} = icmp eq i{} %r{}, 0\n", output, {zero, bits, right})
|
||||
try check_failure(state, zero, .division_by_zero, instruction.span)
|
||||
divisor := right
|
||||
if (!ir.unsigned_integer(instruction.type) and instruction.op == .divide) {
|
||||
minimum :: fresh(state)
|
||||
negative_one :: fresh(state)
|
||||
overflow :: fresh(state)
|
||||
try io.print(" %r{} = icmp eq i{} %r{}, {}\n", output, {minimum, bits, left, enummap.get(&ir.integer_minima, instruction.type)?})
|
||||
try io.print(" %r{} = icmp eq i{} %r{}, -1\n", output, {negative_one, bits, right})
|
||||
try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {overflow, minimum, negative_one})
|
||||
try check_failure(state, overflow, .integer_overflow, instruction.span)
|
||||
}
|
||||
if (!ir.unsigned_integer(instruction.type) and instruction.op == .remainder) {
|
||||
negative_one :: fresh(state)
|
||||
divisor = fresh(state)
|
||||
try io.print(" %r{} = icmp eq i{} %r{}, -1\n", output, {negative_one, bits, right})
|
||||
# LLVM srem is poison for min/-1; every signed value rem -1 is zero.
|
||||
try io.print(" %r{} = select i1 %r{}, i{} 1, i{} %r{}\n", output, {divisor, negative_one, bits, bits, right})
|
||||
}
|
||||
opcode []u8 := if (ir.unsigned_integer(instruction.type)) "udiv" else "sdiv"
|
||||
if (instruction.op == .remainder)
|
||||
opcode = if (ir.unsigned_integer(instruction.type)) "urem" else "srem"
|
||||
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {result, opcode, bits, left, divisor})
|
||||
} else {
|
||||
opcode :: enummap.get(&integer_opcodes, instruction.op)?
|
||||
bits :: ir.integer_bits(instruction.type)
|
||||
sign []u8 :: if (ir.unsigned_integer(instruction.type)) "u" else "s"
|
||||
pair :: fresh(state)
|
||||
overflow :: fresh(state)
|
||||
try io.print(" %r{} = call {{ i{}, i1 }} @llvm.{s}{s}.with.overflow.i{}(i{} %r{}, i{} %r{})\n", output, {pair, bits, sign, opcode, bits, bits, left, bits, right})
|
||||
try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 1\n", output, {overflow, bits, pair})
|
||||
try check_failure(state, overflow, .integer_overflow, instruction.span)
|
||||
try io.print(" %r{} = extractvalue {{ i{}, i1 }} %r{}, 0\n", output, {result, bits, pair})
|
||||
}
|
||||
}
|
||||
.equal, .not_equal, .less, .less_equal, .greater, .greater_equal: {
|
||||
left :: try load(state, instruction.left)
|
||||
right :: try load(state, instruction.right)
|
||||
operand_type :: state.values[usize(instruction.left)]
|
||||
if (operand_type.kind == .float) {
|
||||
predicate :: enummap.get(&float_predicates, instruction.op)?
|
||||
try io.print(" %r{} = fcmp {s} double %r{}, %r{}\n", output, {result, predicate, left, right})
|
||||
} else {
|
||||
prefix []u8 := ""
|
||||
if (instruction.op != .equal and instruction.op != .not_equal) {
|
||||
if (ir.unsigned_integer(operand_type.kind)) prefix = "u"
|
||||
else prefix = "s"
|
||||
}
|
||||
predicate :: enummap.get(&integer_predicates, instruction.op)?
|
||||
try io.print(" %r{} = icmp {s}{s} ", output, {result, prefix, predicate})
|
||||
try write_type(output, state.module, operand_type)
|
||||
try io.print(" %r{}, %r{}\n", output, {left, right})
|
||||
}
|
||||
}
|
||||
}
|
||||
try store(state, instruction.destination, result)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_shift proc(state @mut State, instruction ir.Instruction) usize ! io.WriteError {
|
||||
output :: state.output
|
||||
left :: try load(state, instruction.left)
|
||||
right :: try load(state, instruction.right)
|
||||
bits :: ir.integer_bits(instruction.type)
|
||||
count_bits :: ir.integer_bits(state.values[usize(instruction.right)].kind)
|
||||
invalid :: fresh(state)
|
||||
try io.print(" %r{} = icmp uge i{} %r{}, {}\n", output, {invalid, count_bits, right, bits})
|
||||
count := right
|
||||
if (instruction.op == .shift_left_saturating) {
|
||||
# keep every evaluated LLVM shift in range, including the discarded saturation path.
|
||||
count = fresh(state)
|
||||
try io.print(" %r{} = select i1 %r{}, i{} 0, i{} %r{}\n", output, {count, invalid, count_bits, count_bits, right})
|
||||
} else {
|
||||
try check_failure(state, invalid, .invalid_shift_count, instruction.span)
|
||||
}
|
||||
if (count_bits != bits) {
|
||||
converted :: fresh(state)
|
||||
conversion []u8 :: if (count_bits < bits) "zext" else "trunc"
|
||||
try io.print(" %r{} = {s} i{} %r{} to i{}\n", output, {converted, conversion, count_bits, count, bits})
|
||||
count = converted
|
||||
}
|
||||
shifted :: fresh(state)
|
||||
signed :: !ir.unsigned_integer(instruction.type)
|
||||
right_opcode []u8 :: if (signed) "ashr" else "lshr"
|
||||
opcode []u8 :: if (instruction.op == .shift_right) right_opcode else "shl"
|
||||
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {shifted, opcode, bits, left, count})
|
||||
if (instruction.op != .shift_left_saturating) return shifted
|
||||
restored :: fresh(state)
|
||||
lost :: fresh(state)
|
||||
overflow :: fresh(state)
|
||||
nonzero :: fresh(state)
|
||||
saturate :: fresh(state)
|
||||
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {restored, right_opcode, bits, shifted, count})
|
||||
try io.print(" %r{} = icmp ne i{} %r{}, %r{}\n", output, {lost, bits, restored, left})
|
||||
try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {overflow, invalid, lost})
|
||||
try io.print(" %r{} = icmp ne i{} %r{}, 0\n", output, {nonzero, bits, left})
|
||||
try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {saturate, overflow, nonzero})
|
||||
limit :: fresh(state)
|
||||
if (signed) {
|
||||
negative :: fresh(state)
|
||||
try io.print(" %r{} = icmp slt i{} %r{}, 0\n", output, {negative, bits, left})
|
||||
try io.print(" %r{} = select i1 %r{}, i{} {}, i{} {}\n", output, {limit, negative, bits, enummap.get(&ir.integer_minima, instruction.type)?, bits, enummap.get(&ir.integer_maxima, instruction.type)?})
|
||||
} else {
|
||||
try io.print(" %r{} = or i{} 0, {}\n", output, {limit, bits, enummap.get(&ir.integer_maxima, instruction.type)?})
|
||||
}
|
||||
result :: fresh(state)
|
||||
try io.print(" %r{} = select i1 %r{}, i{} %r{}, i{} %r{}\n", output, {result, saturate, bits, limit, bits, shifted})
|
||||
return result
|
||||
}
|
||||
|
||||
#! compares the complete sentinel value, including nested aggregate fields.
|
||||
@hide:file
|
||||
sentinel_difference proc(state @mut State, type Type, actual, expected usize) usize ! io.WriteError {
|
||||
output :: state.output
|
||||
if (ir.aggregate(type.kind)) {
|
||||
shape :: state.module.arrays.items[usize(type.shape)]
|
||||
count usize :: if (shape.is_union) divtrunc!(shape.byte_size, 8) else if (shape.kind == .record) shape.fields.len else shape.len
|
||||
result := fresh(state)
|
||||
try io.print(" %r{} = icmp ne i1 false, false\n", output, {result})
|
||||
for (0..count) |index| {
|
||||
child :: if (shape.is_union) Type{ kind = .u64 } else if (shape.kind == .record) Type{ kind = shape.fields[index].type, shape = shape.fields[index].array_type } else Type{ kind = shape.element, shape = shape.element_array }
|
||||
left :: fresh(state)
|
||||
right :: fresh(state)
|
||||
try io.print(" %r{} = extractvalue ", output, {left})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(" %r{}, ", output, {actual})
|
||||
if (shape.is_union) try io.write_all(output, "0, ")
|
||||
try io.print("{}\n %r{} = extractvalue ", output, {index, right})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(" %r{}, ", output, {expected})
|
||||
if (shape.is_union) try io.write_all(output, "0, ")
|
||||
try io.print("{}\n", output, {index})
|
||||
different :: try sentinel_difference(state, child, left, right)
|
||||
combined :: fresh(state)
|
||||
try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {combined, result, different})
|
||||
result = combined
|
||||
}
|
||||
return result
|
||||
}
|
||||
if (type.kind == .string) {
|
||||
left_pointer :: fresh(state)
|
||||
right_pointer :: fresh(state)
|
||||
left_length :: fresh(state)
|
||||
right_length :: fresh(state)
|
||||
try io.print(" %r{} = extractvalue %string %r{}, 0\n", output, {left_pointer, actual})
|
||||
try io.print(" %r{} = extractvalue %string %r{}, 0\n", output, {right_pointer, expected})
|
||||
try io.print(" %r{} = extractvalue %string %r{}, 1\n", output, {left_length, actual})
|
||||
try io.print(" %r{} = extractvalue %string %r{}, 1\n", output, {right_length, expected})
|
||||
pointer_difference :: try sentinel_difference(state, Type{ kind = .pointer }, left_pointer, right_pointer)
|
||||
length_difference :: try sentinel_difference(state, Type{ kind = .usize }, left_length, right_length)
|
||||
result :: fresh(state)
|
||||
try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {result, pointer_difference, length_difference})
|
||||
return result
|
||||
}
|
||||
result :: fresh(state)
|
||||
if (type.kind == .void) {
|
||||
try io.print(" %r{} = icmp ne i1 false, false\n", output, {result})
|
||||
return result
|
||||
}
|
||||
opcode []u8 :: if (type.kind == .float) "fcmp une" else "icmp ne"
|
||||
try io.print(" %r{} = {s} ", output, {result, opcode})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(" %r{}, %r{}\n", output, {actual, expected})
|
||||
return result
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_memory proc(state @mut State, instruction ir.Instruction) void ! io.WriteError {
|
||||
output :: state.output
|
||||
element :: Type{ kind = instruction.pointee, shape = instruction.array_type }
|
||||
width :: ir.storage_size(state.module, element.kind, element.shape)
|
||||
if (width == 0) return
|
||||
destination :: try load(state, instruction.left)
|
||||
operand :: try load(state, instruction.right)
|
||||
count :: try load(state, instruction.extra)
|
||||
size := count
|
||||
if (width != 1) {
|
||||
pair :: fresh(state)
|
||||
overflow :: fresh(state)
|
||||
size = fresh(state)
|
||||
try io.print(" %r{} = call {{ i64, i1 }} @llvm.umul.with.overflow.i64(i64 %r{}, i64 {})\n", output, {pair, count, width})
|
||||
try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair})
|
||||
try check_failure(state, overflow, .integer_overflow, instruction.span)
|
||||
try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 0\n", output, {size, pair})
|
||||
}
|
||||
nonempty :: fresh(state)
|
||||
destination_null :: fresh(state)
|
||||
try io.print(" %r{} = icmp ne i64 %r{}, 0\n", output, {nonempty, count})
|
||||
try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {destination_null, destination})
|
||||
missing := destination_null
|
||||
if (instruction.op == .memory_copy) {
|
||||
source_null :: fresh(state)
|
||||
missing = fresh(state)
|
||||
try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {source_null, operand})
|
||||
try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {missing, destination_null, source_null})
|
||||
}
|
||||
invalid :: fresh(state)
|
||||
try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {invalid, nonempty, missing})
|
||||
try check_failure(state, invalid, .invalid_pointer_access, instruction.span)
|
||||
if (instruction.op == .memory_copy) {
|
||||
destination_bits :: fresh(state)
|
||||
source_bits :: fresh(state)
|
||||
ordered :: fresh(state)
|
||||
forward :: fresh(state)
|
||||
backward :: fresh(state)
|
||||
distance :: fresh(state)
|
||||
overlap :: fresh(state)
|
||||
try io.print(" %r{} = ptrtoint ptr %r{} to i64\n", output, {destination_bits, destination})
|
||||
try io.print(" %r{} = ptrtoint ptr %r{} to i64\n", output, {source_bits, operand})
|
||||
try io.print(" %r{} = icmp uge i64 %r{}, %r{}\n", output, {ordered, destination_bits, source_bits})
|
||||
try io.print(" %r{} = sub i64 %r{}, %r{}\n", output, {forward, destination_bits, source_bits})
|
||||
try io.print(" %r{} = sub i64 %r{}, %r{}\n", output, {backward, source_bits, destination_bits})
|
||||
try io.print(" %r{} = select i1 %r{}, i64 %r{}, i64 %r{}\n", output, {distance, ordered, forward, backward})
|
||||
try io.print(" %r{} = icmp ult i64 %r{}, %r{}\n", output, {overlap, distance, size})
|
||||
try check_failure(state, overlap, .overlapping_memory_copy, instruction.span)
|
||||
try io.print(" call void @llvm.memcpy.p0.p0.i64(ptr %r{}, ptr %r{}, i64 %r{}, i1 false)\n", output, {destination, operand, size})
|
||||
return
|
||||
}
|
||||
label :: fresh(state)
|
||||
index :: fresh(state)
|
||||
next :: fresh(state)
|
||||
active :: fresh(state)
|
||||
pointer :: fresh(state)
|
||||
try io.print(" br label %set_start{}\nset_start{}:\n br label %set_test{}\nset_test{}:\n", output, {label, label, label, label})
|
||||
try io.print(" %r{} = phi i64 [ 0, %set_start{} ], [ %r{}, %set_body{} ]\n", output, {index, label, next, label})
|
||||
try io.print(" %r{} = icmp ult i64 %r{}, %r{}\n", output, {active, index, count})
|
||||
try io.print(" br i1 %r{}, label %set_body{}, label %set_end{}\nset_body{}:\n", output, {active, label, label, label})
|
||||
try io.print(" %r{} = getelementptr ", output, {pointer})
|
||||
try write_type(output, state.module, element)
|
||||
try io.print(", ptr %r{}, i64 %r{}\n", output, {destination, index})
|
||||
try store_payload(state, element, operand, pointer)
|
||||
try io.print(" %r{} = add i64 %r{}, 1\n br label %set_test{}\nset_end{}:\n", output, {next, index, label, label})
|
||||
}
|
||||
|
||||
@hide:file
|
||||
check_union proc(state @mut State, pointer usize, shape ir.ArrayType, field usize, span diagnostics.Span) void ! io.WriteError {
|
||||
if (shape.tag_type == .void) return
|
||||
tag :: fresh(state)
|
||||
failed :: fresh(state)
|
||||
try io.print(" %r{} = load ", state.output, {tag})
|
||||
try write_type(state.output, state.module, Type{ kind = shape.tag_type })
|
||||
try io.print(", ptr %r{}\n %r{} = icmp ne ", state.output, {pointer, failed})
|
||||
try write_type(state.output, state.module, Type{ kind = shape.tag_type })
|
||||
try io.print(" %r{}, ", state.output, {tag})
|
||||
try write_constant(state.output, state.module, shape.fields[field].tag)
|
||||
try io.write_all(state.output, "\n")
|
||||
try check_failure(state, failed, .inactive_union_field, span)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
store_payload proc(state @mut State, type Type, value, pointer usize) void ! io.WriteError {
|
||||
if (type.kind == .void) return
|
||||
if (ir.aggregate(type.kind)) {
|
||||
shape :: state.module.arrays.items[usize(type.shape)]
|
||||
if (!shape.is_union) {
|
||||
count usize :: if (type.kind == .record) shape.fields.len else shape.len
|
||||
for (0..count) |i| {
|
||||
child :: if (type.kind == .record) Type{ kind = shape.fields[i].type, shape = shape.fields[i].array_type } else Type{ kind = shape.element, shape = shape.element_array }
|
||||
offset usize :: if (type.kind == .record) shape.fields[i].byte_offset else i * ir.storage_size(state.module, shape.element, shape.element_array)
|
||||
element :: fresh(state)
|
||||
address :: fresh(state)
|
||||
try io.print(" %r{} = extractvalue ", state.output, {element})
|
||||
try write_type(state.output, state.module, type)
|
||||
try io.print(" %r{}, {}\n %r{} = getelementptr i8, ptr %r{}, i64 {}\n", state.output, {value, i, address, pointer, offset})
|
||||
try store_payload(state, child, element, address)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
try io.write_all(state.output, " store ")
|
||||
try write_type(state.output, state.module, type)
|
||||
try io.print(" %r{}, ptr %r{}, align 1\n", state.output, {value, pointer})
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_conversion proc(state @mut State, instruction ir.Instruction) usize ! io.WriteError {
|
||||
output :: state.output
|
||||
source :: state.values[usize(instruction.left)].kind
|
||||
destination :: instruction.type
|
||||
operand :: try load(state, instruction.left)
|
||||
if (source == .float and destination == .float) return operand
|
||||
source_bits :: ir.integer_bits(source)
|
||||
destination_bits :: ir.integer_bits(destination)
|
||||
source_unsigned :: ir.unsigned_integer(source)
|
||||
destination_unsigned :: ir.unsigned_integer(destination)
|
||||
if (destination == .float) {
|
||||
result :: fresh(state)
|
||||
opcode []u8 :: if (source_unsigned) "uitofp" else "sitofp"
|
||||
try io.print(" %r{} = {s} i{} %r{} to double\n", output, {result, opcode, source_bits, operand})
|
||||
return result
|
||||
}
|
||||
if (source == .float) {
|
||||
# compare the truncated value before fptosi/fptoui, whose out-of-range result is poison.
|
||||
truncated :: fresh(state)
|
||||
lower_valid :: fresh(state)
|
||||
upper_valid :: fresh(state)
|
||||
valid :: fresh(state)
|
||||
failed :: fresh(state)
|
||||
upper :: enummap.get(&conversion_upper_bounds, destination)?
|
||||
try io.print(" %r{} = call double @llvm.trunc.f64(double %r{})\n", output, {truncated, operand})
|
||||
if (destination_unsigned) try io.print(" %r{} = fcmp oge double %r{}, 0.0\n", output, {lower_valid, truncated})
|
||||
else try io.print(" %r{} = fcmp oge double %r{}, -{s}\n", output, {lower_valid, truncated, upper})
|
||||
try io.print(" %r{} = fcmp olt double %r{}, {s}\n", output, {upper_valid, truncated, upper})
|
||||
try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {valid, lower_valid, upper_valid})
|
||||
try io.print(" %r{} = xor i1 %r{}, true\n", output, {failed, valid})
|
||||
try check_failure(state, failed, .integer_overflow, instruction.span)
|
||||
result :: fresh(state)
|
||||
opcode []u8 :: if (destination_unsigned) "fptoui" else "fptosi"
|
||||
try io.print(" %r{} = {s} double %r{} to i{}\n", output, {result, opcode, truncated, destination_bits})
|
||||
return result
|
||||
}
|
||||
if (!source_unsigned and (destination_unsigned or destination_bits < source_bits)) {
|
||||
failed :: fresh(state)
|
||||
try io.print(" %r{} = icmp slt i{} %r{}, {}\n", output, {failed, source_bits, operand, enummap.get(&ir.integer_minima, destination)?})
|
||||
try check_failure(state, failed, .integer_overflow, instruction.span)
|
||||
}
|
||||
if (destination_bits < source_bits or (source_unsigned and !destination_unsigned and destination_bits == source_bits)) {
|
||||
failed :: fresh(state)
|
||||
predicate []u8 :: if (source_unsigned) "ugt" else "sgt"
|
||||
try io.print(" %r{} = icmp {s} i{} %r{}, {}\n", output, {failed, predicate, source_bits, operand, enummap.get(&ir.integer_maxima, destination)?})
|
||||
try check_failure(state, failed, .integer_overflow, instruction.span)
|
||||
}
|
||||
if (source_bits == destination_bits) return operand
|
||||
result :: fresh(state)
|
||||
opcode []u8 := "trunc"
|
||||
if (source_bits < destination_bits) opcode = if (source_unsigned) "zext" else "sext"
|
||||
try io.print(" %r{} = {s} i{} %r{} to i{}\n", output, {result, opcode, source_bits, operand, destination_bits})
|
||||
return result
|
||||
}
|
||||
|
||||
@hide:file
|
||||
variadic_type proc(type ir.Type) ir.Type {
|
||||
match type {
|
||||
.bool, .i8, .u8, .i16, .u16: return .i32
|
||||
else: return type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
import "@std/mem"
|
||||
import "@std/io"
|
||||
import "@std/enummap"
|
||||
import "@std/arraylist"
|
||||
import "@source/ir"
|
||||
import "@source/diagnostics"
|
||||
|
||||
@hide
|
||||
Type :: struct {
|
||||
kind ir.Type = .void
|
||||
shape ir.ArrayId = ir.no_array
|
||||
}
|
||||
|
||||
@hide
|
||||
State :: struct {
|
||||
output io.Writer
|
||||
module @ir.Module
|
||||
line_starts []arraylist.ArrayList(usize)
|
||||
values []mut Type
|
||||
next_register usize = 0
|
||||
}
|
||||
|
||||
#! writes a standalone native LLVM module; all temporary emitter storage is released before returning.
|
||||
emit proc(
|
||||
allocator mem.Allocator,
|
||||
output io.Writer,
|
||||
module @ir.Module,
|
||||
diagnostic_store @diagnostics.Store,
|
||||
sources []diagnostics.Source,
|
||||
primary diagnostics.SourceId,
|
||||
) void ! (mem.AllocError | io.WriteError) {
|
||||
line_starts := arraylist.init(arraylist.ArrayList(usize), allocator)
|
||||
defer {
|
||||
for (line_starts.items) |@starts| arraylist.deinit(starts)
|
||||
arraylist.deinit(&line_starts)
|
||||
}
|
||||
for (sources) |source| {
|
||||
starts := arraylist.init(usize, allocator)
|
||||
errdefer arraylist.deinit(&starts)
|
||||
try arraylist.append(&starts, usize(0))
|
||||
for (source.text) |byte, index| {
|
||||
if (byte == '\n') try arraylist.append(&starts, index + 1)
|
||||
}
|
||||
try arraylist.append(&line_starts, starts)
|
||||
}
|
||||
try io.write_all(output, runtime)
|
||||
for (sources) |source, index| {
|
||||
try io.print("\n@p{} = private constant ", output, {index})
|
||||
try bytes_constant(output, source.path, false)
|
||||
}
|
||||
try io.print("\n@paths = private constant [{} x %string] [", output, {sources.len})
|
||||
for (sources) |source, index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
try io.print("%string {{ ptr @p{}, i64 {} }}", output, {index, source.path.len})
|
||||
}
|
||||
try io.write_all(output, "]\n")
|
||||
for (module.strings.items) |string, index| {
|
||||
try io.print("@s{} = private constant ", output, {index})
|
||||
try bytes_constant(output, string.allocation[..string.len], true)
|
||||
try io.write_all(output, "\n")
|
||||
}
|
||||
for (diagnostic_store.list.items) |diagnostic, index| {
|
||||
try io.print("@d{} = private constant ", output, {index})
|
||||
try bytes_constant(output, diagnostics.message(diagnostic), false)
|
||||
try io.write_all(output, "\n")
|
||||
}
|
||||
memory_codes [_]diagnostics.Code :: [
|
||||
.null_unwrap, .invalid_slice_bounds, .sentinel_mismatch,
|
||||
.overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access,
|
||||
.division_by_zero,
|
||||
.invalid_shift_count,
|
||||
.unreachable_executed,
|
||||
]
|
||||
for (memory_codes) |code| {
|
||||
try io.print("@memory_{s} = private constant ", output, {enummap.get(&memory_symbols, code)?})
|
||||
try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, code)?, false)
|
||||
try io.write_all(output, "\n")
|
||||
}
|
||||
try emit_reporting(output, diagnostic_store, line_starts.items, sources, primary)
|
||||
for (module.globals.items) |global, index| {
|
||||
try io.print("@g{} = internal global ", output, {index})
|
||||
storage_kind :: if (global.type == .void) ir.Type.invalid else global.type
|
||||
try write_type(output, module, Type{ kind = storage_kind, shape = global.array_type })
|
||||
try io.print(" zeroinitializer\n@gs{} = internal global i8 0\n", output, {index})
|
||||
try emit_global(output, module, global, index)
|
||||
}
|
||||
for (module.arrays.items) |shape, index| {
|
||||
if (shape.kind == .record) try emit_record_printer(output, module, ir.ArrayId(index))
|
||||
else try emit_array_printer(output, module, ir.ArrayId(index))
|
||||
}
|
||||
for (module.functions.items) |function, index| {
|
||||
if (function.external_name.len != 0) {
|
||||
duplicate := false
|
||||
for (module.functions.items[..index]) |previous| {
|
||||
if (mem.eql(previous.external_name, function.external_name)) duplicate = true
|
||||
}
|
||||
if (
|
||||
duplicate
|
||||
or mem.eql(function.external_name, "write")
|
||||
or mem.eql(function.external_name, "__error")
|
||||
or mem.eql(function.external_name, "snprintf")
|
||||
or mem.eql(function.external_name, "exit")
|
||||
) continue
|
||||
}
|
||||
values :: try mem.alloc(Type, allocator, function.value_count)
|
||||
defer mem.free(allocator, values)
|
||||
mem.set(values, Type{})
|
||||
for (function.blocks.items) |block| {
|
||||
for (block.instructions.items) |instruction| {
|
||||
if (instruction.destination != ir.no_value) values[usize(instruction.destination)] = Type{ kind = instruction.type, shape = instruction.array_type }
|
||||
}
|
||||
}
|
||||
state := State{ output = output, module = module, line_starts = line_starts.items, values = values }
|
||||
try emit_function(&state, &function, index)
|
||||
}
|
||||
try io.write_all(output, "define i32 @main() {\nentry:\n")
|
||||
if (module.entry) |entry| {
|
||||
function :: module.functions.items[usize(entry)]
|
||||
result :: Type{ kind = function.result, shape = function.result_array_type }
|
||||
if (result.kind != .void) try io.write_all(output, " %r0 = ")
|
||||
else try io.write_all(output, " ")
|
||||
try io.write_all(output, "call ")
|
||||
try write_abi_result(output, module, result)
|
||||
try io.write_all(output, " ")
|
||||
try function_symbol(output, module, entry)
|
||||
try io.write_all(output, "()\n")
|
||||
printable := result
|
||||
operand usize := 0
|
||||
if (function.result_fallible) {
|
||||
shape :: module.arrays.items[usize(result.shape)]
|
||||
success :: shape.fields[0]
|
||||
try io.write_all(output, " %result = alloca ")
|
||||
try write_type(output, module, result)
|
||||
try io.write_all(output, "\n store ")
|
||||
try write_type(output, module, result)
|
||||
try io.write_all(output, " %r0, ptr %result\n %tag = load ")
|
||||
try write_type(output, module, Type{ kind = shape.tag_type })
|
||||
try io.write_all(output, ", ptr %result\n %success = icmp eq ")
|
||||
try write_type(output, module, Type{ kind = shape.tag_type })
|
||||
try io.write_all(output, " %tag, ")
|
||||
try write_constant(output, module, success.tag)
|
||||
try io.write_all(output, "\n br i1 %success, label %ok, label %error\nerror:\n call void @report(i64 -1)\n ret i32 1\nok:\n")
|
||||
printable = Type{ kind = success.type, shape = success.array_type }
|
||||
operand = 1
|
||||
if (printable.kind != .void) {
|
||||
try io.print(" %payload = getelementptr i8, ptr %result, i64 {}\n %r1 = load ", output, {shape.payload_offset})
|
||||
try write_type(output, module, printable)
|
||||
try io.write_all(output, ", ptr %payload\n")
|
||||
}
|
||||
}
|
||||
if (printable.kind != .void and printable.kind != .invalid) {
|
||||
try print_call(output, module, printable, operand)
|
||||
try io.write_all(output, " call void @out(i32 1, ptr @newline, i64 1)\n")
|
||||
}
|
||||
try io.write_all(output, " call void @report(i64 -1)\n")
|
||||
status i32 :: if (diagnostics.count(diagnostic_store) == 0) 0 else 1
|
||||
try io.print(" ret i32 {}\n", output, {status})
|
||||
} else {
|
||||
try io.print(" call void @trap(i64 {}, i64 0, i64 0, i64 0, ptr null, i64 0)\n unreachable\n", output, {u32(module.entry_diagnostic)})
|
||||
}
|
||||
try io.write_all(output, "}\n")
|
||||
}
|
||||
|
||||
@hide:file
|
||||
bytes_constant proc(output io.Writer, bytes []u8, terminated bool) void ! io.WriteError {
|
||||
size usize :: if (terminated) bytes.len + 1 else bytes.len
|
||||
try io.print("[{} x i8] c\"", output, {size})
|
||||
digits :: "0123456789ABCDEF"
|
||||
for (bytes) |byte| {
|
||||
escaped [3]u8 :: ['\\', digits[usize(divtrunc!(byte, 16))], digits[usize(rem!(byte, 16))]]
|
||||
try io.write_all(output, (&escaped)[..])
|
||||
}
|
||||
if (terminated) try io.write_all(output, "\\00")
|
||||
try io.write_all(output, "\"")
|
||||
}
|
||||
|
||||
@hide
|
||||
write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError {
|
||||
match type.kind {
|
||||
.i8, .u8: try io.write_all(output, "i8")
|
||||
.i16, .u16: try io.write_all(output, "i16")
|
||||
.i32, .u32: try io.write_all(output, "i32")
|
||||
.int, .isize, .u64, .usize: try io.write_all(output, "i64")
|
||||
.float: try io.write_all(output, "double")
|
||||
.bool: try io.write_all(output, "i1")
|
||||
.string: try io.write_all(output, "%string")
|
||||
.function, .pointer: try io.write_all(output, "ptr")
|
||||
.address: try io.write_all(output, "%address")
|
||||
.array: {
|
||||
shape :: module.arrays.items[usize(type.shape)]
|
||||
try io.print("[{} x ", output, {shape.len})
|
||||
try write_type(output, module, Type{ kind = shape.element, shape = shape.element_array })
|
||||
try io.write_all(output, "]")
|
||||
}
|
||||
.record: {
|
||||
shape :: module.arrays.items[usize(type.shape)]
|
||||
if (shape.is_union) {
|
||||
words usize :: divtrunc!(shape.byte_size, 8)
|
||||
try io.print("{{ [{} x i64] }}", output, {words})
|
||||
return
|
||||
}
|
||||
try io.write_all(output, "{ ")
|
||||
for (shape.fields) |field, index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
try write_type(output, module, Type{ kind = field.type, shape = field.array_type })
|
||||
}
|
||||
try io.write_all(output, " }")
|
||||
}
|
||||
.void: try io.write_all(output, "void")
|
||||
# invalid storage only belongs to recovery paths that trap before reading it.
|
||||
.invalid: try io.write_all(output, "i8")
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
abi_extensions :: $enummap.init_default(ir.Type, []u8, "", {
|
||||
i8 = "signext",
|
||||
i16 = "signext",
|
||||
u8 = "zeroext",
|
||||
u16 = "zeroext",
|
||||
bool = "zeroext",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
abi_extension proc(kind ir.Type) []u8 {
|
||||
if (enummap.get(&abi_extensions, kind)) |extension| return extension
|
||||
return ""
|
||||
}
|
||||
|
||||
@hide
|
||||
write_abi_result proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError {
|
||||
extension :: abi_extension(type.kind)
|
||||
if (extension.len != 0) try io.print("{s} ", output, {extension})
|
||||
try write_type(output, module, type)
|
||||
}
|
||||
|
||||
@hide
|
||||
write_abi_parameter proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError {
|
||||
try write_type(output, module, type)
|
||||
extension :: abi_extension(type.kind)
|
||||
if (extension.len != 0) try io.print(" {s}", output, {extension})
|
||||
}
|
||||
|
||||
@hide
|
||||
function_symbol proc(output io.Writer, module @ir.Module, id ir.FunctionId) void ! io.WriteError {
|
||||
name :: module.functions.items[usize(id)].external_name
|
||||
if (name.len == 0) {
|
||||
try io.print("@f{}", output, {u32(id)})
|
||||
return
|
||||
}
|
||||
try io.write_all(output, "@\"")
|
||||
digits :: "0123456789ABCDEF"
|
||||
for (name) |byte| {
|
||||
escaped [3]u8 :: ['\\', digits[usize(divtrunc!(byte, 16))], digits[usize(rem!(byte, 16))]]
|
||||
try io.write_all(output, (&escaped)[..])
|
||||
}
|
||||
try io.write_all(output, "\"")
|
||||
}
|
||||
|
||||
@hide:file
|
||||
local_type proc(function @ir.Function, index usize) Type {
|
||||
type := Type{ kind = function.locals.items[index] }
|
||||
if (type.kind == .void) type.kind = .invalid
|
||||
if (ir.aggregate(type.kind)) {
|
||||
for (function.array_storage.items) |storage| {
|
||||
if (storage.local and usize(storage.index) == index) {
|
||||
type.shape = storage.array_type
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
@hide
|
||||
Location :: struct {
|
||||
line usize = 1
|
||||
column usize = 1
|
||||
}
|
||||
|
||||
@hide
|
||||
location proc(sources []arraylist.ArrayList(usize), span diagnostics.Span) Location {
|
||||
line_starts :: sources[usize(span.source)].items
|
||||
offset :: usize(span.start)
|
||||
line := usize(0)
|
||||
end := line_starts.len
|
||||
while (line + 1 < end) {
|
||||
middle :: line + divtrunc!(end - line, 2)
|
||||
if (line_starts[middle] <= offset) line = middle
|
||||
else end = middle
|
||||
}
|
||||
return Location{ line = line + 1, column = offset - line_starts[line] + 1 }
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_reporting proc(
|
||||
output io.Writer,
|
||||
diagnostic_store @diagnostics.Store,
|
||||
line_starts []arraylist.ArrayList(usize),
|
||||
sources []diagnostics.Source,
|
||||
primary diagnostics.SourceId,
|
||||
) void ! io.WriteError {
|
||||
try io.write_all(output, "define internal void @io_failure() noreturn {\nentry:\n")
|
||||
try io.print(" %path_result = call i64 @write(i32 2, ptr @p{}, i64 {})\n", output, {usize(primary), sources[usize(primary)].path.len})
|
||||
try io.write_all(output, " %message_result = call i64 @write(i32 2, ptr @write_error, i64 44)\n call void @exit(i32 2)\n unreachable\n}\n")
|
||||
try io.write_all(output, "define internal void @diagnose(i64 %source, i64 %line, i64 %column, ptr %message, i64 %length, i1 %trap) {\nentry:\n")
|
||||
try io.print(" %descriptor = getelementptr [{} x %string], ptr @paths, i64 0, i64 %source\n", output, {sources.len})
|
||||
try io.write_all(output, " %path = load %string, ptr %descriptor\n %bytes = extractvalue %string %path, 0\n %path_length = extractvalue %string %path, 1\n call void @out(i32 2, ptr %bytes, i64 %path_length)\n")
|
||||
try io.write_all(output,
|
||||
` call void @out(i32 2, ptr @colon, i64 1)
|
||||
` call void @print_integer(i32 2, i64 %line, i1 true)
|
||||
` call void @out(i32 2, ptr @colon, i64 1)
|
||||
` call void @print_integer(i32 2, i64 %column, i1 true)
|
||||
` %label = select i1 %trap, ptr @runtime_trap, ptr @error
|
||||
` %label_length = select i1 %trap, i64 16, i64 9
|
||||
` call void @out(i32 2, ptr %label, i64 %label_length)
|
||||
` call void @out(i32 2, ptr %message, i64 %length)
|
||||
` call void @out(i32 2, ptr @newline, i64 1)
|
||||
` ret void
|
||||
`}
|
||||
)
|
||||
try io.write_all(output, "define internal void @report(i64 %trap) {\nentry:\n")
|
||||
for (diagnostic_store.list.items) |diagnostic, index| {
|
||||
position :: location(line_starts, diagnostic.span)
|
||||
message :: diagnostics.message(diagnostic)
|
||||
try io.print(" %t{} = icmp eq i64 %trap, {}\n", output, {index, index})
|
||||
try io.print(" call void @diagnose(i64 {}, i64 {}, i64 {}, ptr @d{}, i64 {}, i1 %t{})\n", output, {
|
||||
usize(diagnostic.span.source), position.line, position.column, index, message.len, index,
|
||||
})
|
||||
}
|
||||
try io.write_all(output, " ret void\n}\n")
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_global proc(output io.Writer, module @ir.Module, global ir.Global, index usize) void ! io.WriteError {
|
||||
type :: Type{ kind = global.type, shape = global.array_type }
|
||||
try io.print("define internal ptr @global{}(i64 %source, i64 %line, i64 %column) {{\nentry:\n", output, {index})
|
||||
try io.print(" %state = load i8, ptr @gs{}\n", output, {index})
|
||||
try io.write_all(output,
|
||||
` switch i8 %state, label %initialize [ i8 1, label %cycle i8 2, label %ready ]
|
||||
`cycle:
|
||||
` call void @trap(i64 -1, i64 %source, i64 %line, i64 %column, ptr @cycle, i64 27)
|
||||
` unreachable
|
||||
`initialize:
|
||||
)
|
||||
try io.print(" store i8 1, ptr @gs{}\n", output, {index})
|
||||
if (type.kind == .void) {
|
||||
try io.print(" call void @f{}()\n", output, {u32(global.initializer)})
|
||||
} else {
|
||||
try io.write_all(output, " %value = call ")
|
||||
try write_abi_result(output, module, type)
|
||||
try io.print(" @f{}()\n store ", output, {u32(global.initializer)})
|
||||
try write_type(output, module, type)
|
||||
try io.print(" %value, ptr @g{}\n", output, {index})
|
||||
}
|
||||
try io.print(" store i8 2, ptr @gs{}\n br label %ready\nready:\n ret ptr @g{}\n}}\n", output, {index, index})
|
||||
}
|
||||
|
||||
@hide:file
|
||||
print_call proc(output io.Writer, module @ir.Module, type Type, operand usize) void ! io.WriteError {
|
||||
match type.kind {
|
||||
.i8, .i16, .i32, .int, .isize, .u8, .u16, .u32, .u64, .usize: {
|
||||
bits :: ir.integer_bits(type.kind)
|
||||
unsigned :: ir.unsigned_integer(type.kind)
|
||||
if (bits < 64) {
|
||||
extension []u8 :: if (unsigned) "zext" else "sext"
|
||||
try io.print(" %print_value{} = {s} i{} %r{} to i64\n", output, {operand, extension, bits, operand})
|
||||
try io.print(" call void @print_integer(i32 1, i64 %print_value{}, i1 {})\n", output, {operand, unsigned})
|
||||
} else try io.print(" call void @print_integer(i32 1, i64 %r{}, i1 {})\n", output, {operand, unsigned})
|
||||
}
|
||||
.float: try io.print(" call void @print_float(double %r{})\n", output, {operand})
|
||||
.bool: try io.print(" call void @print_bool(i1 %r{})\n", output, {operand})
|
||||
.string: try io.print(" call void @print_string(%string %r{})\n", output, {operand})
|
||||
.pointer: try io.write_all(output, " call void @out(i32 1, ptr @pointer_text, i64 9)\n")
|
||||
.array, .record: {
|
||||
try io.print(" call void @print_array{}(", output, {u32(type.shape)})
|
||||
try write_type(output, module, type)
|
||||
try io.print(" %r{})\n", output, {operand})
|
||||
}
|
||||
else: unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_array_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void ! io.WriteError {
|
||||
shape :: module.arrays.items[usize(id)]
|
||||
type :: Type{ kind = .array, shape = id }
|
||||
element :: Type{ kind = shape.element, shape = shape.element_array }
|
||||
try io.print("define internal void @print_array{}(", output, {u32(id)})
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, " %value) {\nentry:\n %storage = alloca ")
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, "\n store ")
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, " %value, ptr %storage\n call void @out(i32 1, ptr @honey.array_open, i64 1)\n br label %loop\nloop:\n %index = phi i64 [ 0, %entry ], [ %next, %element ]\n")
|
||||
try io.print(" %done = icmp eq i64 %index, {}\n", output, {shape.len})
|
||||
try io.write_all(output, " br i1 %done, label %end, label %separator\nseparator:\n %first = icmp eq i64 %index, 0\n br i1 %first, label %element, label %comma\ncomma:\n call void @out(i32 1, ptr @separator, i64 2)\n br label %element\nelement:\n %pointer = getelementptr ")
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, ", ptr %storage, i64 0, i64 %index\n %r0 = load ")
|
||||
try write_type(output, module, element)
|
||||
try io.write_all(output, ", ptr %pointer\n")
|
||||
# functions may occur inside arrays without being printable source results.
|
||||
if (element.kind != .function) try print_call(output, module, element, 0)
|
||||
else try io.write_all(output, " call void @exit(i32 2)\n")
|
||||
try io.write_all(output, " %next = add i64 %index, 1\n br label %loop\nend:\n call void @out(i32 1, ptr @honey.array_close, i64 1)\n ret void\n}\n")
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_record_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void ! io.WriteError {
|
||||
shape :: module.arrays.items[usize(id)]
|
||||
type :: Type{ kind = .record, shape = id }
|
||||
if (shape.is_union) {
|
||||
try emit_union_printer(output, module, id)
|
||||
return
|
||||
}
|
||||
try io.print("define internal void @print_array{}(", output, {u32(id)})
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, " %value) {\nentry:\n call void @out(i32 1, ptr @record_open, i64 1)\n")
|
||||
for (shape.fields) |field, index| {
|
||||
if (index != 0) try io.write_all(output, " call void @out(i32 1, ptr @separator, i64 2)\n")
|
||||
try io.print(" %r{} = extractvalue ", output, {index})
|
||||
try write_type(output, module, type)
|
||||
try io.print(" %value, {}\n", output, {index})
|
||||
if (field.type != .function) try print_call(output, module, Type{ kind = field.type, shape = field.array_type }, index)
|
||||
else try io.write_all(output, " call void @exit(i32 2)\n")
|
||||
}
|
||||
try io.write_all(output, " call void @out(i32 1, ptr @record_close, i64 1)\n ret void\n}\n")
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_union_printer proc(output io.Writer, module @ir.Module, id ir.ArrayId) void ! io.WriteError {
|
||||
shape :: module.arrays.items[usize(id)]
|
||||
type :: Type{ kind = .record, shape = id }
|
||||
try io.print("define internal void @print_array{}(", output, {u32(id)})
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, " %value) {\nentry:\n %storage = alloca ")
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, "\n store ")
|
||||
try write_type(output, module, type)
|
||||
try io.write_all(output, " %value, ptr %storage\n call void @out(i32 1, ptr @record_open, i64 1)\n")
|
||||
if (shape.tag_type != .void) {
|
||||
try io.write_all(output, " %tag = load ")
|
||||
try write_type(output, module, Type{ kind = shape.tag_type })
|
||||
try io.write_all(output, ", ptr %storage\n")
|
||||
target :: if (shape.fields.len == 0) " br label %end\n" else " br label %test0\n"
|
||||
try io.write_all(output, target)
|
||||
for (shape.fields) |field, index| {
|
||||
try io.print("test{}:\n %tag_match{} = icmp eq ", output, {index, index})
|
||||
try write_type(output, module, Type{ kind = shape.tag_type })
|
||||
try io.write_all(output, " %tag, ")
|
||||
try write_constant(output, module, field.tag)
|
||||
try io.print("\n br i1 %tag_match{}, label %case{}, label %", output, {index, index})
|
||||
if (index + 1 == shape.fields.len) try io.write_all(output, "end\n")
|
||||
else try io.print("test{}\n", output, {index + 1})
|
||||
}
|
||||
} else {
|
||||
target :: if (shape.fields.len == 0) " br label %end\n" else " br label %case0\n"
|
||||
try io.write_all(output, target)
|
||||
}
|
||||
for (shape.fields) |field, index| {
|
||||
if (shape.tag_type == .void and index != 0) break
|
||||
try io.print("case{}:\n", output, {index})
|
||||
if (field.type != .void) {
|
||||
try io.print(" %p{} = getelementptr i8, ptr %storage, i64 {}\n %r{} = load ", output, {index, shape.payload_offset, index})
|
||||
try write_type(output, module, Type{ kind = field.type, shape = field.array_type })
|
||||
try io.print(", ptr %p{}\n", output, {index})
|
||||
if (field.type != .function) try print_call(output, module, Type{ kind = field.type, shape = field.array_type }, index)
|
||||
else try io.write_all(output, " call void @exit(i32 2)\n")
|
||||
}
|
||||
try io.write_all(output, " br label %end\n")
|
||||
}
|
||||
try io.write_all(output, "end:\n call void @out(i32 1, ptr @record_close, i64 1)\n ret void\n}\n")
|
||||
}
|
||||
|
||||
@hide
|
||||
fresh proc(state @mut State) usize {
|
||||
value :: state.next_register
|
||||
state.next_register += 1
|
||||
return value
|
||||
}
|
||||
|
||||
@hide
|
||||
load proc(state @mut State, value ir.ValueId) usize ! io.WriteError {
|
||||
result :: fresh(state)
|
||||
try io.print(" %r{} = load ", state.output, {result})
|
||||
try write_type(state.output, state.module, state.values[usize(value)])
|
||||
try io.print(", ptr %v{}\n", state.output, {u32(value)})
|
||||
return result
|
||||
}
|
||||
|
||||
@hide
|
||||
store proc(state @State, destination ir.ValueId, register usize) void ! io.WriteError {
|
||||
if (destination == ir.no_value or state.values[usize(destination)].kind == .void) return
|
||||
try io.write_all(state.output, " store ")
|
||||
try write_type(state.output, state.module, state.values[usize(destination)])
|
||||
try io.print(" %r{}, ptr %v{}\n", state.output, {register, u32(destination)})
|
||||
}
|
||||
|
||||
@hide
|
||||
check_local proc(state @State, slot u32, read bool, span diagnostics.Span) void ! io.WriteError {
|
||||
position :: location(state.line_starts, span)
|
||||
try io.print(" call void @check_init(ptr %i{}, i1 {}, i64 {}, i64 {}, i64 {})\n", state.output, {slot, read, usize(span.source), position.line, position.column})
|
||||
}
|
||||
|
||||
@hide:file
|
||||
memory_symbols :: $enummap.init_default(diagnostics.Code, []u8, "", {
|
||||
null_unwrap = "null_unwrap",
|
||||
invalid_slice_bounds = "invalid_slice_bounds",
|
||||
sentinel_mismatch = "sentinel_mismatch",
|
||||
overlapping_memory_copy = "overlapping_memory_copy",
|
||||
unsupported_foreign_call = "unsupported_foreign_call",
|
||||
invalid_pointer_access = "invalid_pointer_access",
|
||||
division_by_zero = "division_by_zero",
|
||||
invalid_shift_count = "invalid_shift_count",
|
||||
unreachable_executed = "unreachable_executed",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
trap_symbols :: $enummap.init_default(diagnostics.Code, []u8, "", {
|
||||
integer_overflow = "overflow",
|
||||
index_out_of_bounds = "bounds",
|
||||
inactive_union_field = "inactive_union",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
trap proc(state @State, code diagnostics.Code, span diagnostics.Span) void ! io.WriteError {
|
||||
position :: location(state.line_starts, span)
|
||||
symbol :: enummap.get(&trap_symbols, code)
|
||||
message :: enummap.get(diagnostics.Code, []u8, &diagnostics.messages, code)?
|
||||
length :: message.len
|
||||
try io.print(" call void @trap(i64 -1, i64 {}, i64 {}, i64 {}, ptr @", state.output, {usize(span.source), position.line, position.column})
|
||||
if (symbol) |name| try io.write_all(state.output, name)
|
||||
else try io.print("memory_{s}", state.output, {enummap.get(&memory_symbols, code)?})
|
||||
try io.print(", i64 {})\n unreachable\n", state.output, {length})
|
||||
}
|
||||
|
||||
@hide
|
||||
check_failure proc(state @mut State, condition usize, code diagnostics.Code, span diagnostics.Span) void ! io.WriteError {
|
||||
label :: fresh(state)
|
||||
try io.print(" br i1 %r{}, label %fail{}, label %ok{}\nfail{}:\n", state.output, {condition, label, label, label})
|
||||
try trap(state, code, span)
|
||||
try io.print("ok{}:\n", state.output, {label})
|
||||
}
|
||||
|
||||
@hide
|
||||
write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError {
|
||||
match value.type {
|
||||
.i8, .i16, .i32, .int, .isize: try io.print("{}", output, {value.data.integer})
|
||||
.u8, .u16, .u32, .u64, .usize: try io.print("{}", output, {value.data.unsigned})
|
||||
.float: {
|
||||
bits :: ptrcast!(@u64, &value.data.real)^
|
||||
try io.print("bitcast (i64 {} to double)", output, {bits})
|
||||
}
|
||||
.bool: try io.print("{}", output, {value.data.boolean})
|
||||
.string: try io.print("{{ ptr @s{}, i64 {} }}", output, {u32(value.data.string), ir.string_bytes(module, value.data.string).len})
|
||||
.pointer: {
|
||||
if (value.data.unsigned == maxval!(u64)) try io.write_all(output, "null")
|
||||
else unreachable
|
||||
}
|
||||
.function: {
|
||||
if (value.data.function == maxval!(ir.FunctionId)) try io.write_all(output, "null")
|
||||
else try function_symbol(output, module, value.data.function)
|
||||
}
|
||||
.array: {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
try io.write_all(output, "[")
|
||||
for (0..shape.len) |index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
element :: ir.aggregate_item(module, value, index)
|
||||
try write_type(output, module, Type{ kind = element.type, shape = element.array_type })
|
||||
try io.write_all(output, " ")
|
||||
try write_constant(output, module, element)
|
||||
}
|
||||
try io.write_all(output, "]")
|
||||
}
|
||||
.record: {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
if (shape.is_union) {
|
||||
words usize :: divtrunc!(shape.byte_size, 8)
|
||||
try io.print("{{ [{} x i64] [", output, {words})
|
||||
for (0..words) |index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
word :: ir.read_storage(module, .u64, ir.no_array, value.data.elements[index * 8..], true)
|
||||
try io.print("i64 {}", output, {word.data.unsigned})
|
||||
}
|
||||
try io.write_all(output, "] }")
|
||||
return
|
||||
}
|
||||
try io.write_all(output, "{ ")
|
||||
for (shape.fields) |_, index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
field :: ir.aggregate_item(module, value, index)
|
||||
try write_type(output, module, Type{ kind = field.type, shape = field.array_type })
|
||||
try io.write_all(output, " ")
|
||||
try write_constant(output, module, field)
|
||||
}
|
||||
try io.write_all(output, " }")
|
||||
}
|
||||
else: unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_function proc(state @mut State, function @ir.Function, id usize) void ! io.WriteError {
|
||||
output :: state.output
|
||||
if (function.external_name.len != 0) try io.write_all(output, "declare ")
|
||||
else try io.write_all(output, "define internal ")
|
||||
try write_abi_result(output, state.module, Type{ kind = function.result, shape = function.result_array_type })
|
||||
try io.write_all(output, " ")
|
||||
try function_symbol(output, state.module, ir.FunctionId(id))
|
||||
try io.write_all(output, "(")
|
||||
for (function.parameters.items) |_, index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
try write_abi_parameter(output, state.module, local_type(function, index))
|
||||
try io.print(" %p{}", output, {index})
|
||||
}
|
||||
if (function.variadic) {
|
||||
if (function.parameters.items.len != 0) try io.write_all(output, ", ")
|
||||
try io.write_all(output, "...")
|
||||
}
|
||||
if (function.external_name.len != 0) {
|
||||
try io.write_all(output, ")\n")
|
||||
return
|
||||
}
|
||||
try io.write_all(output, ") {\nentry:\n")
|
||||
for (function.locals.items) |_, index| {
|
||||
type :: local_type(function, index)
|
||||
try io.print(" %l{} = alloca ", output, {index})
|
||||
try write_type(output, state.module, type)
|
||||
try io.print("\n %i{} = alloca i1\n store i1 {}, ptr %i{}\n", output, {index, index < function.parameters.items.len, index})
|
||||
if (index < function.parameters.items.len) {
|
||||
try io.write_all(output, " store ")
|
||||
try write_type(output, state.module, type)
|
||||
try io.print(" %p{}, ptr %l{}\n", output, {index, index})
|
||||
}
|
||||
}
|
||||
for (state.values) |type, index| {
|
||||
if (type.kind == .void) continue
|
||||
try io.print(" %v{} = alloca ", output, {index})
|
||||
try write_type(output, state.module, type)
|
||||
try io.write_all(output, "\n")
|
||||
}
|
||||
try io.print(" br label %b{}\n", output, {u32(function.entry)})
|
||||
for (function.blocks.items) |block, index| {
|
||||
try io.print("b{}:\n", output, {index})
|
||||
for (block.instructions.items) |instruction| try emit_instruction(state, &block, instruction)
|
||||
terminator :: block.terminator
|
||||
match terminator.kind {
|
||||
.return_value: {
|
||||
if (terminator.value == ir.no_value or state.values[usize(terminator.value)].kind == .void) try io.write_all(output, " ret void\n")
|
||||
else {
|
||||
result :: try load(state, terminator.value)
|
||||
try io.write_all(output, " ret ")
|
||||
try write_type(output, state.module, state.values[usize(terminator.value)])
|
||||
try io.print(" %r{}\n", output, {result})
|
||||
}
|
||||
}
|
||||
.trap: try io.print(" call void @trap(i64 {}, i64 0, i64 0, i64 0, ptr null, i64 0)\n unreachable\n", output, {u32(terminator.diagnostic)})
|
||||
.unreachable: try trap(state, .unreachable_executed, terminator.span)
|
||||
.jump: try io.print(" br label %b{}\n", output, {u32(terminator.target)})
|
||||
.branch: {
|
||||
condition :: try load(state, terminator.value)
|
||||
try io.print(" br i1 %r{}, label %b{}, label %b{}\n", output, {condition, u32(terminator.target), u32(terminator.alternative)})
|
||||
}
|
||||
}
|
||||
}
|
||||
try io.write_all(output, "}\n")
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
@hide
|
||||
runtime ::
|
||||
`target triple = "arm64-apple-macosx11.0.0"
|
||||
`%string = type { ptr, i64 }
|
||||
`%address = type { ptr, ptr }
|
||||
`declare i64 @write(i32, ptr, i64)
|
||||
`declare ptr @__error()
|
||||
`declare i32 @snprintf(ptr, i64, ptr, ...)
|
||||
`declare void @exit(i32) noreturn
|
||||
`declare double @llvm.trunc.f64(double)
|
||||
`declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)
|
||||
`declare { i8, i1 } @llvm.sadd.with.overflow.i8(i8, i8)
|
||||
`declare { i8, i1 } @llvm.ssub.with.overflow.i8(i8, i8)
|
||||
`declare { i8, i1 } @llvm.smul.with.overflow.i8(i8, i8)
|
||||
`declare { i8, i1 } @llvm.uadd.with.overflow.i8(i8, i8)
|
||||
`declare { i8, i1 } @llvm.usub.with.overflow.i8(i8, i8)
|
||||
`declare { i8, i1 } @llvm.umul.with.overflow.i8(i8, i8)
|
||||
`declare { i16, i1 } @llvm.sadd.with.overflow.i16(i16, i16)
|
||||
`declare { i16, i1 } @llvm.ssub.with.overflow.i16(i16, i16)
|
||||
`declare { i16, i1 } @llvm.smul.with.overflow.i16(i16, i16)
|
||||
`declare { i16, i1 } @llvm.uadd.with.overflow.i16(i16, i16)
|
||||
`declare { i16, i1 } @llvm.usub.with.overflow.i16(i16, i16)
|
||||
`declare { i16, i1 } @llvm.umul.with.overflow.i16(i16, i16)
|
||||
`declare { i32, i1 } @llvm.sadd.with.overflow.i32(i32, i32)
|
||||
`declare { i32, i1 } @llvm.ssub.with.overflow.i32(i32, i32)
|
||||
`declare { i32, i1 } @llvm.smul.with.overflow.i32(i32, i32)
|
||||
`declare { i32, i1 } @llvm.uadd.with.overflow.i32(i32, i32)
|
||||
`declare { i32, i1 } @llvm.usub.with.overflow.i32(i32, i32)
|
||||
`declare { i32, i1 } @llvm.umul.with.overflow.i32(i32, i32)
|
||||
`declare { i64, i1 } @llvm.sadd.with.overflow.i64(i64, i64)
|
||||
`declare { i64, i1 } @llvm.ssub.with.overflow.i64(i64, i64)
|
||||
`declare { i64, i1 } @llvm.smul.with.overflow.i64(i64, i64)
|
||||
`declare { i64, i1 } @llvm.uadd.with.overflow.i64(i64, i64)
|
||||
`declare { i64, i1 } @llvm.usub.with.overflow.i64(i64, i64)
|
||||
`declare { i64, i1 } @llvm.umul.with.overflow.i64(i64, i64)
|
||||
`@fmt_int = private constant [5 x i8] c"%lld\00"
|
||||
`@fmt_uint = private constant [5 x i8] c"%llu\00"
|
||||
`@fmt_float = private constant [6 x i8] c"%.17g\00"
|
||||
`@true = private constant [4 x i8] c"true"
|
||||
`@false = private constant [5 x i8] c"false"
|
||||
`@pointer_text = private constant [9 x i8] c"<pointer>"
|
||||
`@colon = private constant [1 x i8] c":"
|
||||
`@error = private constant [9 x i8] c": error: "
|
||||
`@runtime_trap = private constant [16 x i8] c": runtime trap: "
|
||||
`@newline = private constant [1 x i8] c"\0A"
|
||||
`@honey.array_open = private constant [1 x i8] c"["
|
||||
`@honey.array_close = private constant [1 x i8] c"]"
|
||||
`@record_open = private constant [1 x i8] c"{"
|
||||
`@record_close = private constant [1 x i8] c"}"
|
||||
`@separator = private constant [2 x i8] c", "
|
||||
`@write_error = private constant [44 x i8] c": infrastructure error: cannot write output\0A"
|
||||
`@honey.uninitialized_read = private constant [42 x i8] c"local value was read before initialization"
|
||||
`@assign = private constant [46 x i8] c"local value was assigned before initialization"
|
||||
`@cycle = private constant [27 x i8] c"global initialization cycle"
|
||||
`@overflow = private constant [27 x i8] c"integer arithmetic overflow"
|
||||
`@bounds = private constant [19 x i8] c"index out of bounds"
|
||||
`@inactive_union = private constant [32 x i8] c"tagged union field is not active"
|
||||
`define internal void @out(i32 %fd, ptr %bytes, i64 %len) {
|
||||
`entry:
|
||||
` br label %loop
|
||||
`loop:
|
||||
` %offset = phi i64 [ 0, %entry ], [ %next, %progress ], [ %offset, %retry ]
|
||||
` %done = icmp eq i64 %offset, %len
|
||||
` br i1 %done, label %end, label %send
|
||||
`send:
|
||||
` %remaining = sub i64 %len, %offset
|
||||
` %pointer = getelementptr i8, ptr %bytes, i64 %offset
|
||||
` %count = call i64 @write(i32 %fd, ptr %pointer, i64 %remaining)
|
||||
` %positive = icmp sgt i64 %count, 0
|
||||
` br i1 %positive, label %progress, label %check
|
||||
`progress:
|
||||
` %next = add i64 %offset, %count
|
||||
` br label %loop
|
||||
`check:
|
||||
` %negative = icmp slt i64 %count, 0
|
||||
` br i1 %negative, label %errno, label %fail
|
||||
`errno:
|
||||
` %error_pointer = call ptr @__error()
|
||||
` %error_value = load i32, ptr %error_pointer
|
||||
` %interrupted = icmp eq i32 %error_value, 4
|
||||
` br i1 %interrupted, label %retry, label %fail
|
||||
`retry:
|
||||
` br label %loop
|
||||
`fail:
|
||||
` call void @io_failure()
|
||||
` unreachable
|
||||
`end:
|
||||
` ret void
|
||||
`}
|
||||
`define internal void @print_integer(i32 %fd, i64 %value, i1 %unsigned) {
|
||||
`entry:
|
||||
` %buffer = alloca [64 x i8], align 16
|
||||
` %format = select i1 %unsigned, ptr @fmt_uint, ptr @fmt_int
|
||||
` %count = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buffer, i64 64, ptr %format, i64 %value)
|
||||
` %valid_low = icmp sge i32 %count, 0
|
||||
` %valid_high = icmp slt i32 %count, 64
|
||||
` %valid = and i1 %valid_low, %valid_high
|
||||
` br i1 %valid, label %output, label %fail
|
||||
`output:
|
||||
` %length = zext i32 %count to i64
|
||||
` call void @out(i32 %fd, ptr %buffer, i64 %length)
|
||||
` ret void
|
||||
`fail:
|
||||
` call void @io_failure()
|
||||
` unreachable
|
||||
`}
|
||||
`define internal void @print_float(double %value) {
|
||||
`entry:
|
||||
` %buffer = alloca [64 x i8], align 16
|
||||
` %count = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buffer, i64 64, ptr @fmt_float, double %value)
|
||||
` %valid_low = icmp sge i32 %count, 0
|
||||
` %valid_high = icmp slt i32 %count, 64
|
||||
` %valid = and i1 %valid_low, %valid_high
|
||||
` br i1 %valid, label %output, label %fail
|
||||
`output:
|
||||
` %length = zext i32 %count to i64
|
||||
` call void @out(i32 1, ptr %buffer, i64 %length)
|
||||
` ret void
|
||||
`fail:
|
||||
` call void @io_failure()
|
||||
` unreachable
|
||||
`}
|
||||
`define internal void @print_bool(i1 %value) {
|
||||
`entry:
|
||||
` %bytes = select i1 %value, ptr @true, ptr @false
|
||||
` %length = select i1 %value, i64 4, i64 5
|
||||
` call void @out(i32 1, ptr %bytes, i64 %length)
|
||||
` ret void
|
||||
`}
|
||||
`define internal void @print_string(%string %value) {
|
||||
`entry:
|
||||
` %bytes = extractvalue %string %value, 0
|
||||
` %length = extractvalue %string %value, 1
|
||||
` call void @out(i32 1, ptr %bytes, i64 %length)
|
||||
` ret void
|
||||
`}
|
||||
`define internal void @trap(i64 %id, i64 %source, i64 %line, i64 %column, ptr %message, i64 %length) noreturn {
|
||||
`entry:
|
||||
` call void @report(i64 %id)
|
||||
` %generated = icmp eq i64 %id, -1
|
||||
` br i1 %generated, label %new, label %end
|
||||
`new:
|
||||
` call void @diagnose(i64 %source, i64 %line, i64 %column, ptr %message, i64 %length, i1 true)
|
||||
` br label %end
|
||||
`end:
|
||||
` call void @exit(i32 1)
|
||||
` unreachable
|
||||
`}
|
||||
`define internal void @check_init(ptr %flag, i1 %read, i64 %source, i64 %line, i64 %column) {
|
||||
`entry:
|
||||
` %global = icmp eq ptr %flag, null
|
||||
` br i1 %global, label %end, label %check
|
||||
`check:
|
||||
` %ready = load i1, ptr %flag
|
||||
` br i1 %ready, label %end, label %fail
|
||||
`fail:
|
||||
` %message = select i1 %read, ptr @honey.uninitialized_read, ptr @assign
|
||||
` %length = select i1 %read, i64 42, i64 46
|
||||
` call void @trap(i64 -1, i64 %source, i64 %line, i64 %column, ptr %message, i64 %length)
|
||||
` unreachable
|
||||
`end:
|
||||
` ret void
|
||||
`}
|
||||
@@ -0,0 +1,183 @@
|
||||
import "@std/mem"
|
||||
import "@std/io"
|
||||
import "@std/fs"
|
||||
import "@std/arraylist"
|
||||
|
||||
SourceError :: enum { source_too_large }
|
||||
|
||||
Error :: alias (
|
||||
fs.Error
|
||||
| SourceError
|
||||
| io.OpenError
|
||||
| io.CloseError
|
||||
| io.ReadError
|
||||
)
|
||||
|
||||
#! returns caller-owned storage; release it with mem.free using the same allocator.
|
||||
@hide:file
|
||||
copy proc(allocator mem.Allocator, text []u8) []mut u8 ! mem.AllocError {
|
||||
bytes :: try mem.alloc(u8, allocator, text.len)
|
||||
memcopy!(bytes, text)
|
||||
return bytes
|
||||
}
|
||||
|
||||
#! returns an owned canonical package directory for either a file or directory input.
|
||||
#! free the returned slice with mem.free using the same allocator; temporary storage is released before returning.
|
||||
directory proc(allocator mem.Allocator, path []u8) []mut u8 ! Error {
|
||||
resolved :: try fs.canonical(allocator, path)
|
||||
errdefer mem.free(allocator, resolved)
|
||||
if (try fs.kind(allocator, resolved) == .directory) return resolved
|
||||
|
||||
end := resolved.len
|
||||
while (end > 1 and resolved[end - 1] != '/') end -= 1
|
||||
parent_text :: if (end == 1) resolved[..1] else resolved[..end - 1]
|
||||
parent :: try copy(allocator, parent_text)
|
||||
mem.free(allocator, resolved)
|
||||
return parent
|
||||
}
|
||||
|
||||
#! returns a caller-owned joined path; release it with mem.free using the same allocator.
|
||||
@hide:file
|
||||
join proc(allocator mem.Allocator, base, name []u8) []mut u8 ! mem.AllocError {
|
||||
separator :: if (base.len == 1 and base[0] == '/') usize(0) else usize(1)
|
||||
if (
|
||||
base.len > maxval!(usize) - separator
|
||||
or name.len > maxval!(usize) - base.len - separator
|
||||
) return .out_of_memory
|
||||
|
||||
bytes :: try mem.alloc(u8, allocator, base.len + separator + name.len)
|
||||
memcopy!(bytes[..base.len], base)
|
||||
if (separator != 0) bytes[base.len] = '/'
|
||||
memcopy!(bytes[base.len + separator..], name)
|
||||
return bytes
|
||||
}
|
||||
|
||||
@hide:file
|
||||
exists proc(allocator mem.Allocator, base, name []u8) bool ! Error {
|
||||
path :: try join(allocator, base, name)
|
||||
defer mem.free(allocator, path)
|
||||
return try fs.exists(allocator, path)
|
||||
}
|
||||
|
||||
#! finds the nearest ancestor build.hon/build.bro, otherwise uses the supplied directory.
|
||||
#! free the returned slice with mem.free using the same allocator; temporary search storage is released before returning.
|
||||
project_root proc(allocator mem.Allocator, package_dir []u8) []mut u8 ! Error {
|
||||
end := package_dir.len
|
||||
while true {
|
||||
base :: package_dir[..end]
|
||||
if (
|
||||
try exists(allocator, base, "build.hon")
|
||||
or try exists(allocator, base, "build.bro")
|
||||
) return try copy(allocator, base)
|
||||
|
||||
if (end == 1) break
|
||||
while (end > 1 and package_dir[end - 1] != '/') end -= 1
|
||||
if (end > 1) end -= 1
|
||||
}
|
||||
return try copy(allocator, package_dir)
|
||||
}
|
||||
|
||||
#! relative imports start at the importing package; @ imports start at the project root.
|
||||
#! free the returned slice with mem.free using the same allocator; temporary path storage is released before returning.
|
||||
resolve proc(allocator mem.Allocator, root_dir, package_dir, import_path []u8) []mut u8 ! Error {
|
||||
if (import_path.len == 0 or import_path[0] == '/') return .invalid_path
|
||||
for (import_path) |byte| {
|
||||
if (byte == 0) return .invalid_path
|
||||
}
|
||||
base := package_dir
|
||||
path := import_path
|
||||
if (path[0] == '@') {
|
||||
base = root_dir
|
||||
path = path[1..]
|
||||
if (path.len == 0 or path[0] == '/') return .invalid_path
|
||||
}
|
||||
joined :: try join(allocator, base, path)
|
||||
defer mem.free(allocator, joined)
|
||||
return try fs.canonical(allocator, joined)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
ends_with proc(text, suffix []u8) bool {
|
||||
return text.len >= suffix.len and mem.eql(text[text.len - suffix.len..], suffix)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
before proc(left, right []u8) bool {
|
||||
count :: if (left.len < right.len) left.len else right.len
|
||||
for 0..count |index| {
|
||||
if (left[index] != right[index]) return left[index] < right[index]
|
||||
}
|
||||
return left.len < right.len
|
||||
}
|
||||
|
||||
#! lists production sources in bytewise order; free each path with mem.free using the same allocator.
|
||||
#! release the list with arraylist.deinit, which uses its retained allocator; temporary directory storage is released before returning.
|
||||
files proc(allocator mem.Allocator, package_dir []u8) arraylist.ArrayList([]u8) ! Error {
|
||||
handle := try fs.open_directory(allocator, package_dir)
|
||||
closing := false
|
||||
defer { if (!closing) fs.close_directory(&handle) catch |_| {} }
|
||||
result := arraylist.init([]u8, allocator)
|
||||
errdefer {
|
||||
for (result.items) |path| mem.free(allocator, path)
|
||||
arraylist.deinit(&result)
|
||||
}
|
||||
while (try fs.next(&handle)) |entry| {
|
||||
if (
|
||||
entry.kind != .file
|
||||
and entry.kind != .link
|
||||
and entry.kind != .unknown
|
||||
) continue
|
||||
name :: entry.name
|
||||
if (
|
||||
(!ends_with(name, ".hon") and !ends_with(name, ".bro"))
|
||||
or ends_with(name, ".test.hon")
|
||||
or ends_with(name, ".test.bro")
|
||||
) continue
|
||||
path :: try join(allocator, package_dir, name)
|
||||
errdefer mem.free(allocator, path)
|
||||
if (entry.kind != .file and try fs.kind(allocator, path) != .file) {
|
||||
mem.free(allocator, path)
|
||||
continue
|
||||
}
|
||||
try arraylist.append(&result, path)
|
||||
}
|
||||
closing = true
|
||||
try fs.close_directory(&handle)
|
||||
# in-place heapsort: deterministic O(n log n), no additional allocation.
|
||||
count := result.items.len
|
||||
start :: divtrunc!(count, 2)
|
||||
for (0..start) |i| sift(result.items, start - i - 1, count)
|
||||
while (count > 1) {
|
||||
count -= 1
|
||||
temporary :: result.items[0]
|
||||
result.items[0] = result.items[count]
|
||||
result.items[count] = temporary
|
||||
sift(result.items, 0, count)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@hide:file
|
||||
sift proc(items []mut []u8, start, count usize) void {
|
||||
root := start
|
||||
while (root < divtrunc!(count, 2)) {
|
||||
child := root * 2 + 1
|
||||
if (child + 1 < count and before(items[child], items[child + 1])) child += 1
|
||||
if (!before(items[root], items[child])) break
|
||||
temporary :: items[root]
|
||||
items[root] = items[child]
|
||||
items[child] = temporary
|
||||
root = child
|
||||
}
|
||||
}
|
||||
|
||||
#! the compiler limits source size to its u32 span address space; storage remains caller-owned.
|
||||
#! release the returned list with arraylist.deinit, which uses its retained allocator.
|
||||
read_source proc(allocator mem.Allocator, runtime io.Io, path []u8) arraylist.ArrayList(u8) ! Error {
|
||||
return fs.read_file(allocator, runtime, path, usize(maxval!(u32))) catch |err| {
|
||||
match err {
|
||||
.limit_exceeded: return .source_too_large
|
||||
else: return err
|
||||
}
|
||||
}
|
||||
}
|
||||
+1827
-222
File diff suppressed because it is too large
Load Diff
+264
-19
@@ -20,14 +20,14 @@ compile_owned proc(text []u8, diagnostic_store @mut diagnostics.Store) ir.Module
|
||||
source :: try mem.alloc(u8, mem.c_allocator, text.len)
|
||||
defer mem.free(mem.c_allocator, source)
|
||||
for (text) |byte, i| source[i] = byte
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &strings, diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, diagnostic_store)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &strings, diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, diagnostic_store)
|
||||
defer typecheck.deinit(&checked)
|
||||
@@ -132,14 +132,14 @@ executes_forward_lazy_globals_and_function_aliases test {
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`forward :: later + 1
|
||||
`alias :: add
|
||||
`forwarded :: add
|
||||
`later :: 18
|
||||
`add proc(left int, right int) int {
|
||||
` local_alias :: alias
|
||||
` local_alias :: forwarded
|
||||
` return left + right
|
||||
`}
|
||||
`main proc() int {
|
||||
` invoke :: alias
|
||||
` invoke :: forwarded
|
||||
` return invoke(forward, 23)
|
||||
`}
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
@@ -174,6 +174,66 @@ supports_bare_void_value_returns_and_fallthrough test {
|
||||
}
|
||||
}
|
||||
|
||||
constructs_present_optionals_without_losing_nested_null_or_repeating_values test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`next proc(calls @mut i16) i16 {
|
||||
` calls^ = calls^ + 1
|
||||
` return 42
|
||||
`}
|
||||
`nested proc() ??i16 { return some!(null) }
|
||||
`Box :: struct { values [2]i16 }
|
||||
`main proc() bool {
|
||||
` calls i16 := 0
|
||||
` absent ??i16 :: null
|
||||
` present ??i16 :: nested()
|
||||
` full ??i16 :: some!(some!(42))
|
||||
` exact i16 :: 42
|
||||
` value ?i16 :: some!(exact)
|
||||
` once ?i16 :: some!(next(&calls))
|
||||
` boxed ?Box :: some!(Box{ values = [20, 22] })
|
||||
` pointer ?@i16 :: some!(&exact)
|
||||
` nested_pointer ??@i16 :: some!(null)
|
||||
` return (
|
||||
` absent == null and present != null and present? == null
|
||||
` and full != null and full? != null and full?? == 42
|
||||
` and value? == 42 and once? == 42 and calls == 1
|
||||
` and boxed?.values[0] + boxed?.values[1] == 42
|
||||
` and pointer?^ == 42 and nested_pointer != null and nested_pointer? == null
|
||||
` )
|
||||
`}
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(ir.Type.bool, outcome.value.type)
|
||||
try testing.expect(outcome.value.data.boolean)
|
||||
}
|
||||
|
||||
rejects_some_without_optional_context_wrong_arity_and_inexact_payloads test {
|
||||
sources [6][]u8 :: [
|
||||
"main proc() ?i16 { return some!() }\n",
|
||||
"main proc() ?i16 { return some!(1, 2) }\n",
|
||||
"main proc() bool { return some!(true) }\n",
|
||||
"main proc() ?i16 { return some!(true) }\n",
|
||||
"main proc() ?i16 { value i32 :: 42\nreturn some!(value) }\n",
|
||||
"main proc() ?i16 { return some!(null) }\n",
|
||||
]
|
||||
for (sources) |source, i| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
code diagnostics.Code :: if (i < 2) .argument_count_mismatch else if (i == 2) .invalid_operand else .type_mismatch
|
||||
try expect_trap(outcome, &diagnostic_store, code)
|
||||
}
|
||||
}
|
||||
|
||||
preserves_demanded_diagnostic_identity_and_source_location test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
@@ -469,8 +529,8 @@ limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignmen
|
||||
limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free }
|
||||
|
||||
releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -529,10 +589,10 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &strings, &diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||
defer typecheck.deinit(&checked)
|
||||
@@ -932,7 +992,7 @@ zero_iteration_body_errors_are_checked_but_not_executed test {
|
||||
}
|
||||
}
|
||||
|
||||
loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_overflow test {
|
||||
terminal_true_loops_and_assignment_arithmetic_preserve_runtime_behavior test {
|
||||
sources [5][]u8 :: [
|
||||
"main proc() int {\n while true { return 42 }\n}\n",
|
||||
"main proc() int {\n while false { return 42 }\n}\n",
|
||||
@@ -945,7 +1005,7 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
if (index != 2) {
|
||||
if (index != 0 and index != 2) {
|
||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
|
||||
try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code)
|
||||
@@ -1684,7 +1744,7 @@ array_context_inference_empty_shapes_and_scalar_leaf_families test {
|
||||
"main proc() string {\n a [2]mut string := [\"one\", \"two\"]\n b := a\n a[1] = \"three\"\n return b[1]\n}\n",
|
||||
"plus proc(n int) int { return n + 1 }\nminus proc(n int) int { return n - 1 }\nmain proc() bool {\n a :: [plus, minus]\n return a[0](41) == 42 and a[1](43) == 42\n}\n",
|
||||
"make proc() [2]i64 { return [20, 22] }\nmain proc() bool {\n return make()[0] + ([20, 22])[1] == 42\n}\n",
|
||||
"count :: alias + 1\nalias :: 1\nmain proc() bool {\n a [count][0]bool :: [[], []]\n return true\n}\n",
|
||||
"count :: forwarded + 1\nforwarded :: 1\nmain proc() bool {\n a [count][0]bool :: [[], []]\n return true\n}\n",
|
||||
]
|
||||
for (sources) |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
@@ -1695,11 +1755,8 @@ array_context_inference_empty_shapes_and_scalar_leaf_families test {
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
if (outcome.value.type == .string) {
|
||||
try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "two"))
|
||||
} else {
|
||||
try testing.expect(outcome.value.data.boolean)
|
||||
}
|
||||
if (outcome.value.type == .string) try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "two"))
|
||||
else try testing.expect(outcome.value.data.boolean)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1924,3 +1981,191 @@ empty_array_entry_results_need_no_escaped_allocation test {
|
||||
interpreter.deinit(&outcome)
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
}
|
||||
|
||||
fallible_source_handlers_do_not_catch_runtime_allocation_failures test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`Error :: enum { out_of_memory }
|
||||
`fail proc() int ! Error { return .out_of_memory }
|
||||
`main proc() int { return fail() catch 42 }
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(usize(0), diagnostics.count(&diagnostic_store))
|
||||
succeeded := false
|
||||
failures := usize(0)
|
||||
for (usize(0)..usize(128)) |limit| {
|
||||
state := AllocationState{ remaining = limit }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
outcome := interpreter.run(allocator, &module, &diagnostic_store) catch |error| {
|
||||
try testing.expect(error == .out_of_memory)
|
||||
failures += 1
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
try testing.expect_equal(usize(0), diagnostics.count(&diagnostic_store))
|
||||
continue
|
||||
}
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
interpreter.deinit(&outcome)
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
succeeded = true
|
||||
break
|
||||
}
|
||||
try testing.expect(succeeded and failures > 0)
|
||||
}
|
||||
|
||||
keeps_generated_foreign_declarations_demand_driven_and_traps_interpreted_calls test {
|
||||
source ::
|
||||
`Handle :: opaque
|
||||
`Packet :: c_struct { context ?*mut Handle, callback ?*c_func(_ c_int, _ c_int) void }
|
||||
`unsupported c_func(_ Packet) Packet
|
||||
`variadic c_func(_ c_int, ...) c_int
|
||||
`single c_func(_ c_float) c_float
|
||||
`extended c_func(_ c_longdouble) c_longdouble
|
||||
`double c_func(_ c_double) c_double
|
||||
`malloc c_func(_ c_ulong) ?*mut anyopaque
|
||||
`@hide:file
|
||||
`helper proc() int { if (true) return 42
|
||||
`else return 0 }
|
||||
`main proc() int { return helper() }
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
|
||||
foreign_module := try compile_owned(
|
||||
"realloc c_func(_ ?*mut anyopaque, _ c_ulong) ?*mut anyopaque\nmain proc() bool { return realloc(null, 8) == null }\n",
|
||||
&diagnostic_store,
|
||||
)
|
||||
defer ir.deinit(&foreign_module)
|
||||
foreign_outcome := try interpreter.run(mem.c_allocator, &foreign_module, &diagnostic_store)
|
||||
defer interpreter.deinit(&foreign_outcome)
|
||||
try expect_trap(foreign_outcome, &diagnostic_store, .unsupported_foreign_call)
|
||||
}
|
||||
|
||||
diagnoses_demanded_opaque_values_and_incompatible_opaque_pointers test {
|
||||
sources [2][]u8 :: [
|
||||
"Handle :: opaque\ntake c_func(_ Handle) void\nmain proc() void { take(undefined) }\n",
|
||||
"A :: opaque\nB :: opaque\nmain proc() void { a ?*A :: null\n b ?*B :: a }\n",
|
||||
]
|
||||
for sources |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap != null)
|
||||
}
|
||||
}
|
||||
|
||||
rejects_demanded_unavailable_c_floating_types_without_unknown_names test {
|
||||
sources [2][]u8 :: [
|
||||
"single c_func() c_float\nmain proc() void { single() }\n",
|
||||
"extended c_func() c_longdouble\nmain proc() void { extended() }\n",
|
||||
]
|
||||
for sources |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try expect_trap(outcome, &diagnostic_store, .invalid_operand)
|
||||
for 0..diagnostics.count(&diagnostic_store) |index| {
|
||||
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(index))?
|
||||
try testing.expect(diagnostic.code != .unknown_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preserves_variadic_call_signatures_and_interpreter_foreign_traps test {
|
||||
sources [2][]u8 :: [
|
||||
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { return take(1) }\n",
|
||||
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { return take(1, true, i8(-2), u8(3), i16(-4), u16(5), 6.5) }\n",
|
||||
]
|
||||
for sources |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
try testing.expect(module.functions.items[0].variadic)
|
||||
try testing.expect_equal(usize(1), module.functions.items[0].parameters.items.len)
|
||||
entry :: &module.functions.items[usize(module.entry?)]
|
||||
found := false
|
||||
for entry.blocks.items |block| {
|
||||
for block.instructions.items |instruction| {
|
||||
if (instruction.op != .call) continue
|
||||
found = true
|
||||
try testing.expect(instruction.variadic)
|
||||
try testing.expect_equal(usize(1), instruction.fixed_arguments)
|
||||
}
|
||||
}
|
||||
try testing.expect(found)
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try expect_trap(outcome, &diagnostic_store, .unsupported_foreign_call)
|
||||
}
|
||||
}
|
||||
|
||||
rejects_variadic_arity_fixed_types_and_non_c_trailing_values test {
|
||||
sources [4][]u8 :: [
|
||||
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { return take() }\n",
|
||||
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { return take(true) }\n",
|
||||
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { return take(1, \"text\") }\n",
|
||||
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { values [2]i64 :: [1, 2]\nreturn take(1, values) }\n",
|
||||
]
|
||||
codes [4]diagnostics.Code :: [
|
||||
.argument_count_mismatch, .type_mismatch, .invalid_operand, .invalid_operand,
|
||||
]
|
||||
for sources |source, index| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try expect_trap(outcome, &diagnostic_store, codes[index])
|
||||
}
|
||||
}
|
||||
|
||||
coerces_strings_to_immutable_byte_pointer_arguments test {
|
||||
sources [3][]u8 :: [
|
||||
"take c_func(_ ?*c_char) void\nmain proc() void { take(\"format\") }\n",
|
||||
"take c_func(_ *u8) void\nmain proc() void { take(\"format\") }\n",
|
||||
"take c_func(_ *i8, ...) void\nmain proc() void { take(\"format\", true) }\n",
|
||||
]
|
||||
for sources |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try expect_trap(outcome, &diagnostic_store, .unsupported_foreign_call)
|
||||
}
|
||||
}
|
||||
|
||||
rejects_string_arguments_for_mutable_or_nonbyte_pointers test {
|
||||
sources [2][]u8 :: [
|
||||
"take c_func(_ ?*mut c_char) void\nmain proc() void { take(\"format\") }\n",
|
||||
"take c_func(_ *u16) void\nmain proc() void { take(\"format\") }\n",
|
||||
]
|
||||
for sources |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try expect_trap(outcome, &diagnostic_store, .type_mismatch)
|
||||
}
|
||||
}
|
||||
|
||||
+154
-85
@@ -1,3 +1,4 @@
|
||||
import "@ffi/c"
|
||||
import "@std/mem"
|
||||
import "@std/arraylist"
|
||||
import "@std/enummap"
|
||||
@@ -5,16 +6,10 @@ import "@std/io"
|
||||
import "@std/process"
|
||||
|
||||
import "@source/diagnostics"
|
||||
import "@source/strpool"
|
||||
import "@source/lexer"
|
||||
import "@source/ast"
|
||||
import "@source/parser"
|
||||
import "@source/sema"
|
||||
import "@source/compilation"
|
||||
import "@source/ir"
|
||||
import "@source/lower"
|
||||
import "@source/interpreter"
|
||||
|
||||
typecheck :: import "@source/sema/check"
|
||||
import "@source/llvm"
|
||||
|
||||
test import "@std/mem"
|
||||
test import "@std/enummap"
|
||||
@@ -24,6 +19,7 @@ test import "@std/strmap"
|
||||
test import "@std/meta"
|
||||
test import "@std/io"
|
||||
test import "@std/process"
|
||||
test import "@std/fs"
|
||||
test import "@std/strconv"
|
||||
test import "@source/diagnostics"
|
||||
test import "@source/strpool"
|
||||
@@ -32,21 +28,27 @@ test import "@source/ast"
|
||||
test import "@source/parser"
|
||||
test import "@source/sema"
|
||||
test import "@source/sema/check"
|
||||
test import "@source/sema/specialize"
|
||||
test import "@source/ir"
|
||||
test import "@source/lower"
|
||||
test import "@source/interpreter"
|
||||
test import "@source/compilation"
|
||||
test import "@source/loader"
|
||||
|
||||
@hide:file
|
||||
DriverError :: alias (
|
||||
mem.AllocError
|
||||
| diagnostics.CapacityError
|
||||
| lexer.SourceError
|
||||
| strpool.InternError
|
||||
compilation.Error
|
||||
| io.OpenError
|
||||
| io.CloseError
|
||||
| io.ReadError
|
||||
| io.WriteError
|
||||
| process.RunError
|
||||
| NativeBuildError
|
||||
)
|
||||
|
||||
@hide:file
|
||||
NativeBuildError :: enum { temporary_file_failed, compiler_failed }
|
||||
|
||||
main proc(init process.Init) i32 {
|
||||
return command(mem.c_allocator, init.io, init.args) catch |err| {
|
||||
if (init.args.values.len > 2) {
|
||||
@@ -76,69 +78,129 @@ infrastructure_messages :: $enummap.init(DriverError, []u8, {
|
||||
not_open_for_writing = "output is not writable",
|
||||
write_failed = "cannot write output",
|
||||
no_progress = "output writer made no progress",
|
||||
spawn_failed = "cannot start native compiler",
|
||||
wait_failed = "cannot wait for native compiler",
|
||||
invalid_arguments = "invalid native compiler arguments",
|
||||
temporary_file_failed = "cannot create temporary LLVM file",
|
||||
compiler_failed = "native compiler failed",
|
||||
empty_package = "package contains no production source files",
|
||||
invalid_path = "invalid filesystem path",
|
||||
path_failed = "cannot resolve filesystem path",
|
||||
directory_failed = "cannot read package directory",
|
||||
})
|
||||
|
||||
@hide:file
|
||||
command proc(allocator mem.Allocator, runtime io.Io, args process.Args) i32 ! DriverError {
|
||||
if (args.values.len != 3) {
|
||||
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
|
||||
return 2
|
||||
if (args.values.len == 3) {
|
||||
if (mem.eql(args.values[1], "run")) return try run_file(allocator, runtime, args.values[2])
|
||||
if (mem.eql(args.values[1], "emit-llvm")) return try emit_file(allocator, runtime, args.values[2])
|
||||
}
|
||||
if (!mem.eql(args.values[1], "run")) {
|
||||
try io.print("usage: honey run <file>\n", io.stderr(runtime), {})
|
||||
return 2
|
||||
if (args.values.len == 5 or args.values.len == 6) {
|
||||
if (mem.eql(args.values[1], "build") and mem.eql(args.values[3], "-o") and args.values[4].len != 0) {
|
||||
optimization [;0]u8 := "-O0"
|
||||
if (args.values.len == 6) {
|
||||
optimization = args.values[5]
|
||||
if (!mem.eql(optimization, "-O0") and !mem.eql(optimization, "-O2")) {
|
||||
try usage(io.stderr(runtime))
|
||||
return 2
|
||||
}
|
||||
}
|
||||
return try build_file(allocator, runtime, args.values[2], args.values[4], optimization)
|
||||
}
|
||||
}
|
||||
return try run_file(allocator, runtime, args.values[2])
|
||||
try usage(io.stderr(runtime))
|
||||
return 2
|
||||
}
|
||||
|
||||
@hide:file
|
||||
read_source proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) arraylist.ArrayList(u8) ! DriverError {
|
||||
file :: try io.open(runtime, path, .read_only)
|
||||
usage proc(output io.Writer) void ! io.WriteError {
|
||||
try io.write_all(output, "usage: honey run <file-or-directory>\n")
|
||||
try io.write_all(output, " honey emit-llvm <file-or-directory>\n")
|
||||
try io.write_all(output, " honey build <file-or-directory> -o <executable> [-O0|-O2]\n")
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverError {
|
||||
session := compilation.init(allocator)
|
||||
defer compilation.deinit(&session)
|
||||
diagnostic_store := diagnostics.init(allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
root :: try compilation.load(&session, runtime, path, &diagnostic_store)
|
||||
module := try compilation.compile(&session, root, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
sources :: compilation.source_files(&session)
|
||||
primary :: session.packages.items[usize(root)].sources.items[0]
|
||||
try llvm.emit(allocator, io.stdout(runtime), &module, &diagnostic_store, sources, primary)
|
||||
try report_diagnostics(allocator, io.stderr(runtime), sources, &diagnostic_store, null)
|
||||
return if (diagnostics.count(&diagnostic_store) == 0) 0 else 1
|
||||
}
|
||||
|
||||
@hide:file
|
||||
build_file proc(
|
||||
allocator mem.Allocator,
|
||||
runtime io.Io,
|
||||
path [;0]u8,
|
||||
output [;0]u8,
|
||||
optimization [;0]u8,
|
||||
) i32 ! DriverError {
|
||||
session := compilation.init(allocator)
|
||||
defer compilation.deinit(&session)
|
||||
diagnostic_store := diagnostics.init(allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
root :: try compilation.load(&session, runtime, path, &diagnostic_store)
|
||||
module := try compilation.compile(&session, root, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
|
||||
template [18]mut u8 := undefined
|
||||
memcopy!(template[..], "/tmp/honey-XXXXXX\0")
|
||||
descriptor :: c.mkstemp(ptrcast!(template[..].ptr))
|
||||
if (descriptor < 0) return .temporary_file_failed
|
||||
defer _ = c.unlink(ptrcast!(template[..].ptr))
|
||||
file :: io.File{ io = runtime, handle = io.Handle{ file_desc = descriptor } }
|
||||
closing := false
|
||||
defer {
|
||||
if (!closing) io.close(file) catch |_| {}
|
||||
}
|
||||
bytes := io.read_all(allocator, io.reader(file), usize(maxval!(u32))) catch |err| {
|
||||
match err {
|
||||
.limit_exceeded: return .source_too_large
|
||||
else: return err
|
||||
}
|
||||
}
|
||||
errdefer arraylist.deinit(&bytes)
|
||||
sources :: compilation.source_files(&session)
|
||||
primary :: session.packages.items[usize(root)].sources.items[0]
|
||||
try llvm.emit(allocator, io.writer(file), &module, &diagnostic_store, sources, primary)
|
||||
closing = true
|
||||
try io.close(file)
|
||||
return bytes
|
||||
}
|
||||
|
||||
@hide:file
|
||||
compile proc(allocator mem.Allocator, source []u8, diagnostic_store @mut diagnostics.Store) ir.Module ! DriverError {
|
||||
strpool.strings = strpool.init(allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
scan_state := lexer.init(allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, diagnostic_store)
|
||||
tree := try parser.parse(allocator, scan_state.tokens.items, source, diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try sema.resolve(allocator, &tree, diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
checked := try typecheck.check(allocator, &tree, &resolution, diagnostic_store)
|
||||
defer typecheck.deinit(&checked)
|
||||
return try lower.lower(allocator, &tree, &resolution, &checked, diagnostic_store)
|
||||
temporary [;0]u8 :: template[..].ptr[..17;0]
|
||||
arguments [9][;0]u8 :: [
|
||||
"clang", "-target", "arm64-apple-macosx11.0.0",
|
||||
"-x", "ir", optimization, temporary, "-o", output,
|
||||
]
|
||||
status :: try process.run(allocator, arguments[..])
|
||||
if (status != 0) return .compiler_failed
|
||||
try report_diagnostics(allocator, io.stderr(runtime), sources, &diagnostic_store, null)
|
||||
return if (diagnostics.count(&diagnostic_store) == 0) 0 else 1
|
||||
}
|
||||
|
||||
@hide:file
|
||||
run_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverError {
|
||||
source := try read_source(allocator, runtime, path)
|
||||
defer arraylist.deinit(&source)
|
||||
session := compilation.init(allocator)
|
||||
defer compilation.deinit(&session)
|
||||
diagnostic_store := diagnostics.init(allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile(allocator, source.items, &diagnostic_store)
|
||||
root :: try compilation.load(&session, runtime, path, &diagnostic_store)
|
||||
module := try compilation.compile(&session, root, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome := try interpreter.run(allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
if (outcome.trap == null) try print_result(io.stdout(runtime), &module, outcome.value)
|
||||
try report_diagnostics(allocator, io.stderr(runtime), path, source.items, &diagnostic_store, outcome.trap)
|
||||
if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
|
||||
value := outcome.value
|
||||
failed := false
|
||||
if (outcome.trap == null) {
|
||||
if (module.entry) |entry| {
|
||||
if (module.functions.items[usize(entry)].result_fallible) {
|
||||
failed = ir.union_active(&module, value, 1)
|
||||
if (!failed) value = ir.aggregate_item(&module, value, 0)
|
||||
}
|
||||
}
|
||||
if (!failed) try print_result(io.stdout(runtime), &module, value)
|
||||
}
|
||||
try report_diagnostics(allocator, io.stderr(runtime), compilation.source_files(&session), &diagnostic_store, outcome.trap)
|
||||
if (failed or outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -152,34 +214,32 @@ print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io
|
||||
@hide:file
|
||||
print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError {
|
||||
match value.type {
|
||||
.int: try io.print("{}", output, {value.data.integer})
|
||||
.usize: try io.print("{}", output, {value.data.unsigned})
|
||||
.int, .i8, .i16, .i32, .isize: try io.print("{}", output, {value.data.integer})
|
||||
.usize, .u8, .u16, .u32, .u64: try io.print("{}", output, {value.data.unsigned})
|
||||
.float: try io.print("{d}", output, {value.data.real})
|
||||
.bool: try io.print("{}", output, {value.data.boolean})
|
||||
.string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)})
|
||||
.array: {
|
||||
.pointer: try io.write_all(output, "<pointer>")
|
||||
.array, .record: {
|
||||
shape :: module.arrays.items[usize(value.array_type)]
|
||||
stride usize :: if (shape.element == .array)
|
||||
module.arrays.items[usize(shape.element_array)].cells
|
||||
else
|
||||
1
|
||||
try io.print("[", output, {})
|
||||
for 0..(shape.len) |index| {
|
||||
if (index != 0) try io.print(", ", output, {})
|
||||
element := ir.Value{}
|
||||
if shape.element == .array {
|
||||
start :: index * stride
|
||||
element = ir.Value{
|
||||
type = .array,
|
||||
array_type = shape.element_array,
|
||||
data = ir.ValueData{ elements = value.data.elements[start..(start + stride)] },
|
||||
}
|
||||
} else {
|
||||
element = value.data.elements[index]
|
||||
count :: if (value.type == .array) shape.len else shape.fields.len
|
||||
opening :: if (value.type == .array) "[" else "{"
|
||||
closing :: if (value.type == .array) "]" else "}"
|
||||
try io.write_all(output, opening)
|
||||
if (shape.is_union) {
|
||||
for (shape.fields) |field, index| {
|
||||
if (!ir.union_active(module, value, index)) continue
|
||||
if (field.type != .void) try print_value(output, module, ir.aggregate_item(module, value, index))
|
||||
break
|
||||
}
|
||||
try print_value(output, module, element)
|
||||
try io.write_all(output, closing)
|
||||
return
|
||||
}
|
||||
try io.print("]", output, {})
|
||||
for (0..count) |index| {
|
||||
if (index != 0) try io.write_all(output, ", ")
|
||||
try print_value(output, module, ir.aggregate_item(module, value, index))
|
||||
}
|
||||
try io.write_all(output, closing)
|
||||
}
|
||||
else: unreachable
|
||||
}
|
||||
@@ -189,26 +249,35 @@ print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.
|
||||
report_diagnostics proc(
|
||||
allocator mem.Allocator,
|
||||
output io.Writer,
|
||||
path []u8,
|
||||
source []u8,
|
||||
sources []diagnostics.Source,
|
||||
diagnostic_store @diagnostics.Store,
|
||||
trap ?diagnostics.DiagnosticId,
|
||||
) void ! (mem.AllocError | io.WriteError) {
|
||||
if (diagnostics.count(diagnostic_store) == 0) return
|
||||
starts := arraylist.init(usize, allocator)
|
||||
defer arraylist.deinit(&starts)
|
||||
try arraylist.append(&starts, usize(0))
|
||||
for (source) |byte, index| {
|
||||
if (byte == '\n') try arraylist.append(&starts, index + 1)
|
||||
line_starts := arraylist.init(arraylist.ArrayList(usize), allocator)
|
||||
defer {
|
||||
for (line_starts.items) |@starts| arraylist.deinit(starts)
|
||||
arraylist.deinit(&line_starts)
|
||||
}
|
||||
for (sources) |source| {
|
||||
starts := arraylist.init(usize, allocator)
|
||||
errdefer arraylist.deinit(&starts)
|
||||
try arraylist.append(&starts, usize(0))
|
||||
for (source.text) |byte, index| {
|
||||
if (byte == '\n') try arraylist.append(&starts, index + 1)
|
||||
}
|
||||
try arraylist.append(&line_starts, starts)
|
||||
}
|
||||
for (0..diagnostics.count(diagnostic_store)) |index| {
|
||||
if (diagnostics.get(diagnostic_store, diagnostics.DiagnosticId(index))) |diagnostic| {
|
||||
source :: sources[usize(diagnostic.span.source)]
|
||||
starts :: line_starts.items[usize(diagnostic.span.source)].items
|
||||
offset :: usize(diagnostic.span.start)
|
||||
line := usize(0)
|
||||
end := starts.items.len
|
||||
end := starts.len
|
||||
while (line + 1 < end) {
|
||||
middle :: line + divtrunc!(end - line, 2)
|
||||
if (starts.items[middle] <= offset) line = middle
|
||||
if (starts[middle] <= offset) line = middle
|
||||
else end = middle
|
||||
}
|
||||
label []u8 := "error"
|
||||
@@ -216,11 +285,11 @@ report_diagnostics proc(
|
||||
if (id == diagnostics.DiagnosticId(index)) label = "runtime trap"
|
||||
}
|
||||
try io.print("{s}:{}:{}: {s}: {s}\n", output, {
|
||||
path,
|
||||
source.path,
|
||||
line + 1,
|
||||
offset - starts.items[line] + 1,
|
||||
offset - starts[line] + 1,
|
||||
label,
|
||||
enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?,
|
||||
diagnostics.message(diagnostic),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1463
-248
File diff suppressed because it is too large
Load Diff
+1248
-148
File diff suppressed because it is too large
Load Diff
+3874
-164
File diff suppressed because it is too large
Load Diff
+197
-200
File diff suppressed because it is too large
Load Diff
+445
-61
@@ -1,7 +1,8 @@
|
||||
import "@std/debug"
|
||||
import "@std/mem"
|
||||
import "@std/hashmap"
|
||||
import "@std/strmap"
|
||||
import "@std/arraylist"
|
||||
import "@source/strpool"
|
||||
|
||||
import "@source/ast"
|
||||
import "@source/diagnostics"
|
||||
@@ -11,24 +12,74 @@ BuiltinType :: enum {
|
||||
bool, string,
|
||||
void, i64,
|
||||
f64, usize,
|
||||
i8, i16, i32, isize,
|
||||
u8, u16, u32, u64, uint,
|
||||
anyopaque
|
||||
c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong
|
||||
c_char, c_uchar, c_short, c_ushort
|
||||
type
|
||||
unavailable
|
||||
}
|
||||
|
||||
@hide:file
|
||||
builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
|
||||
{ "int", .int },
|
||||
{ "float", .float },
|
||||
{ "bool", .bool },
|
||||
{ "string", .string },
|
||||
{ "void", .void },
|
||||
{ "i64", .i64 },
|
||||
{ "f64", .f64 },
|
||||
{ "usize", .usize },
|
||||
])
|
||||
Builtin :: struct {
|
||||
name []u8
|
||||
type BuiltinType
|
||||
}
|
||||
|
||||
BindingKind :: enum { none, declaration, builtin, invalid }
|
||||
@hide:file
|
||||
builtins [_]Builtin :: [
|
||||
{ name = "int", type = .int },
|
||||
{ name = "float", type = .float },
|
||||
{ name = "bool", type = .bool },
|
||||
{ name = "string", type = .string },
|
||||
{ name = "void", type = .void },
|
||||
{ name = "i64", type = .i64 },
|
||||
{ name = "f64", type = .f64 },
|
||||
{ name = "usize", type = .usize },
|
||||
{ name = "i8", type = .i8 },
|
||||
{ name = "i16", type = .i16 },
|
||||
{ name = "i32", type = .i32 },
|
||||
{ name = "isize", type = .isize },
|
||||
{ name = "u8", type = .u8 },
|
||||
{ name = "u16", type = .u16 },
|
||||
{ name = "u32", type = .u32 },
|
||||
{ name = "u64", type = .u64 },
|
||||
{ name = "uint", type = .uint },
|
||||
{ name = "anyopaque", type = .anyopaque },
|
||||
{ name = "c_int", type = .c_int },
|
||||
{ name = "c_uint", type = .c_uint },
|
||||
{ name = "c_long", type = .c_long },
|
||||
{ name = "c_ulong", type = .c_ulong },
|
||||
{ name = "c_longlong", type = .c_longlong },
|
||||
{ name = "c_ulonglong", type = .c_ulonglong },
|
||||
{ name = "c_char", type = .c_char },
|
||||
{ name = "c_schar", type = .c_char },
|
||||
{ name = "c_uchar", type = .c_uchar },
|
||||
{ name = "c_short", type = .c_short },
|
||||
{ name = "c_ushort", type = .c_ushort },
|
||||
{ name = "c_double", type = .f64 },
|
||||
{ name = "c_float", type = .unavailable },
|
||||
{ name = "c_longdouble", type = .unavailable },
|
||||
{ name = "type", type = .type },
|
||||
]
|
||||
|
||||
@hide:file
|
||||
intern_builtins proc(
|
||||
$N usize,
|
||||
pool @mut strpool.StringPool,
|
||||
entries @[N]Builtin,
|
||||
) [N]mut strpool.StringId ! (mem.AllocError | strpool.InternError) {
|
||||
ids [N]mut strpool.StringId := undefined
|
||||
for (entries) |builtin, index| ids[index] = try strpool.intern(pool, builtin.name)
|
||||
return ids
|
||||
}
|
||||
|
||||
BindingKind :: enum { none, declaration, namespace, builtin, invalid }
|
||||
|
||||
BindingData :: union {
|
||||
declaration ast.NodeId
|
||||
namespace ast.NodeId
|
||||
builtin BuiltinType
|
||||
diagnostic diagnostics.DiagnosticId
|
||||
}
|
||||
@@ -38,6 +89,14 @@ Binding :: struct {
|
||||
data BindingData = BindingData{ diagnostic = diagnostics.no_diagnostic }
|
||||
}
|
||||
|
||||
#! links a file-local import to a package root; failed loads retain their diagnostic.
|
||||
Import :: struct {
|
||||
node ast.NodeId
|
||||
name strpool.StringId
|
||||
target ?ast.NodeId
|
||||
diagnostic ?diagnostics.DiagnosticId = null
|
||||
}
|
||||
|
||||
@hide:file
|
||||
Storage :: struct {
|
||||
bindings []mut Binding
|
||||
@@ -50,7 +109,7 @@ Resolution :: struct { storage Storage }
|
||||
|
||||
@hide:file
|
||||
Scope :: struct {
|
||||
names hashmap.StringHashMap(Binding)
|
||||
names hashmap.AutoHashMap(strpool.StringId, Binding)
|
||||
parent ?@Scope
|
||||
}
|
||||
|
||||
@@ -59,49 +118,171 @@ State :: struct {
|
||||
tree @ast.Tree
|
||||
diagnostics @mut diagnostics.Store
|
||||
bindings []mut Binding
|
||||
builtin_ids []strpool.StringId
|
||||
allocator mem.Allocator
|
||||
namespaces hashmap.AutoHashMap(ast.NodeId, Scope)
|
||||
aliases []mut ast.NodeId
|
||||
selections arraylist.ArrayList(ast.NodeId)
|
||||
}
|
||||
|
||||
#! borrows `tree` and `diagnostic_store` during resolution, without retaining their storage.
|
||||
#! borrows `tree`, its identifier `pool`, and `diagnostic_store` during resolution.
|
||||
#! `allocator` supplies caller-owned bindings and temporary scopes released before return.
|
||||
#! release the returned Resolution with deinit, which uses the retained allocator.
|
||||
resolve proc(
|
||||
allocator mem.Allocator,
|
||||
tree @ast.Tree,
|
||||
pool @mut strpool.StringPool,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) Resolution ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
) Resolution ! (
|
||||
mem.AllocError
|
||||
| strpool.InternError
|
||||
| diagnostics.CapacityError
|
||||
) {
|
||||
roots [1]ast.NodeId :: [tree.root]
|
||||
return try resolve_packages(
|
||||
allocator,
|
||||
tree,
|
||||
roots[..],
|
||||
&[],
|
||||
pool,
|
||||
diagnostic_store,
|
||||
)
|
||||
}
|
||||
|
||||
#! resolves each package root in its own namespace; AST IDs share the compilation arena.
|
||||
resolve_packages proc(
|
||||
allocator mem.Allocator,
|
||||
tree @ast.Tree,
|
||||
roots []ast.NodeId,
|
||||
imports []Import,
|
||||
pool @mut strpool.StringPool,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) Resolution ! (mem.AllocError | strpool.InternError | diagnostics.CapacityError) {
|
||||
bindings :: try mem.alloc(Binding, allocator, ast.node_count(tree))
|
||||
errdefer mem.free(allocator, bindings)
|
||||
mem.set(bindings, Binding{})
|
||||
builtin_ids :: try intern_builtins(pool, &builtins)
|
||||
aliases :: try mem.alloc(ast.NodeId, allocator, ast.node_count(tree))
|
||||
defer mem.free(allocator, aliases)
|
||||
mem.set(aliases, maxval!(ast.NodeId))
|
||||
|
||||
state := State{
|
||||
tree = tree,
|
||||
diagnostics = diagnostic_store,
|
||||
bindings = bindings,
|
||||
builtin_ids = builtin_ids[..],
|
||||
allocator = allocator,
|
||||
namespaces = hashmap.init(allocator),
|
||||
aliases = aliases,
|
||||
selections = arraylist.init(allocator),
|
||||
}
|
||||
defer arraylist.deinit(&state.selections)
|
||||
defer {
|
||||
for (state.namespaces.entries) |entry| {
|
||||
if (entry.hash == 0) continue
|
||||
scope := entry.value
|
||||
hashmap.deinit(&scope.names)
|
||||
}
|
||||
hashmap.deinit(&state.namespaces)
|
||||
}
|
||||
|
||||
package_scope := Scope{ names = hashmap.init(allocator), parent = null }
|
||||
defer hashmap.deinit(&package_scope.names)
|
||||
for (0..ast.package_decl_count(tree)) |i| {
|
||||
id :: ast.package_decl(tree, i)
|
||||
match ast.decl_kind(tree, id) {
|
||||
.statement: try predeclare(&state, &package_scope, id, ast.statement_decl(tree, id).name)
|
||||
.function: try predeclare(&state, &package_scope, id, ast.function_decl(tree, id).name)
|
||||
.invalid: {}
|
||||
package_scopes :: try mem.alloc(Scope, allocator, roots.len)
|
||||
defer mem.free(allocator, package_scopes)
|
||||
file_scopes :: try mem.alloc(Scope, allocator, ast.source_files(tree).len)
|
||||
defer mem.free(allocator, file_scopes)
|
||||
for (file_scopes) |@scope| scope^ = Scope{ names = hashmap.init(allocator), parent = null }
|
||||
defer { for (file_scopes) |@scope| hashmap.deinit(&scope.names) }
|
||||
for (roots) |root, index| {
|
||||
package_scopes[index] = Scope{ names = hashmap.init(allocator), parent = null }
|
||||
scope := &package_scopes[index]
|
||||
errdefer hashmap.deinit(&scope.names)
|
||||
for (0..ast.package_decl_count_at(tree, root)) |i| {
|
||||
id :: ast.package_decl_at(tree, root, i)
|
||||
file_scopes[usize(ast.node_span(tree, id).source)].parent = scope
|
||||
if (ast.file_local(tree, id)) continue
|
||||
match ast.decl_kind(tree, id) {
|
||||
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
|
||||
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
|
||||
.import, .invalid: {}
|
||||
}
|
||||
}
|
||||
hashmap.put(&state.namespaces, root, scope^) catch |err| {
|
||||
match err {
|
||||
.key_exists: unreachable
|
||||
else: return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (0..ast.package_decl_count(tree)) |i| {
|
||||
id :: ast.package_decl(tree, i)
|
||||
match ast.decl_kind(tree, id) {
|
||||
.statement: try resolve_statement_decl(&state, &package_scope, id)
|
||||
.function: try resolve_function(&state, &package_scope, id)
|
||||
.invalid: resolve_invalid(&state, id)
|
||||
for (roots) |root| {
|
||||
for (0..ast.package_decl_count_at(tree, root)) |i| {
|
||||
id :: ast.package_decl_at(tree, root, i)
|
||||
if (!ast.file_local(tree, id)) continue
|
||||
scope :: &file_scopes[usize(ast.node_span(tree, id).source)]
|
||||
match ast.decl_kind(tree, id) {
|
||||
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
|
||||
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
|
||||
.import, .invalid: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (imports) |imported| try resolve_import(&state, &file_scopes[usize(ast.node_span(tree, imported.node).source)], imported)
|
||||
for (roots) |root| try resolve_package(&state, root, file_scopes)
|
||||
try canonicalize_aliases(&state)
|
||||
|
||||
return Resolution{ storage = Storage{ bindings = bindings, allocator = allocator } }
|
||||
return Resolution{ storage = Storage{
|
||||
bindings = bindings,
|
||||
allocator = allocator,
|
||||
} }
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_package proc(
|
||||
state @mut State, root ast.NodeId, file_scopes []Scope,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
tree := state.tree^
|
||||
tree.root = root
|
||||
for (0..ast.package_decl_count(&tree)) |i| {
|
||||
id :: ast.package_decl(&tree, i)
|
||||
scope :: &file_scopes[usize(ast.node_span(&tree, id).source)]
|
||||
match ast.decl_kind(&tree, id) {
|
||||
.statement: try resolve_statement_decl(state, scope, id)
|
||||
.function: try resolve_function(state, scope, id)
|
||||
.invalid: resolve_invalid(state, id)
|
||||
.import: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_import proc(
|
||||
state @mut State, scope @mut Scope, imported Import,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
span :: ast.import_decl(state.tree, imported.node).span
|
||||
value := if (imported.diagnostic) |diagnostic|
|
||||
Binding{ kind = .invalid, data = BindingData{ diagnostic = diagnostic } }
|
||||
else if (imported.target) |target|
|
||||
Binding{ kind = .namespace, data = BindingData{ namespace = target } }
|
||||
else
|
||||
Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .package_unavailable, span),
|
||||
} }
|
||||
if (imported.name == strpool.no_str) {
|
||||
state.bindings[usize(imported.node)] = value
|
||||
return
|
||||
}
|
||||
if hashmap.get(&scope.names, imported.name) |_| {
|
||||
state.bindings[usize(imported.node)] = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .duplicate_name, span),
|
||||
} }
|
||||
return
|
||||
}
|
||||
if lookup(state, scope.parent, imported.name) |_| {
|
||||
value = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .duplicate_name, span),
|
||||
} }
|
||||
}
|
||||
try put_name(scope, imported.name, value)
|
||||
state.bindings[usize(imported.node)] = value
|
||||
}
|
||||
|
||||
#! releases binding storage with the allocator retained by resolve.
|
||||
@@ -114,20 +295,29 @@ binding proc(resolution @Resolution, id ast.NodeId) Binding {
|
||||
return resolution.storage.bindings[usize(id)]
|
||||
}
|
||||
|
||||
#! replaces owned binding storage after AST elaboration; takes ownership of `bindings`.
|
||||
replace_bindings proc(resolution @mut Resolution, bindings []mut Binding) void {
|
||||
mem.free(resolution.storage.allocator, resolution.storage.bindings)
|
||||
resolution.storage.bindings = bindings
|
||||
}
|
||||
|
||||
@hide:file
|
||||
lookup proc(scope ?@Scope, name []u8) ?Binding {
|
||||
lookup proc(state @State, scope ?@Scope, name strpool.StringId) ?Binding {
|
||||
if scope |current| {
|
||||
if hashmap.get(¤t.names, name) |found| return found
|
||||
return lookup(current.parent, name)
|
||||
return lookup(state, current.parent, name)
|
||||
}
|
||||
if strmap.get(&builtins, name) |type| {
|
||||
return Binding{ kind = .builtin, data = BindingData{ builtin = type } }
|
||||
for (state.builtin_ids) |id, index| {
|
||||
if (id == name) return Binding{
|
||||
kind = .builtin,
|
||||
data = BindingData{ builtin = builtins[index].type },
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@hide:file
|
||||
put_name proc(scope @mut Scope, name []u8, value Binding) void ! mem.AllocError {
|
||||
put_name proc(scope @mut Scope, name strpool.StringId, value Binding) void ! mem.AllocError {
|
||||
hashmap.put(&scope.names, name, value) catch |err| {
|
||||
match err {
|
||||
.key_exists: unreachable
|
||||
@@ -143,7 +333,11 @@ predeclare proc(
|
||||
id ast.NodeId,
|
||||
name_span ast.Span,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
name :: ast.source_text(state.tree, name_span)
|
||||
name :: ast.name_id(state.tree, id)
|
||||
if (mem.eql(ast.source_text(state.tree, name_span), "_")) {
|
||||
state.bindings[usize(id)] = Binding{ kind = .declaration, data = BindingData{ declaration = id } }
|
||||
return
|
||||
}
|
||||
if hashmap.get(&scope.names, name) |_| {
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span)
|
||||
state.bindings[usize(id)] = Binding{
|
||||
@@ -154,7 +348,7 @@ predeclare proc(
|
||||
}
|
||||
|
||||
value := Binding{ kind = .declaration, data = BindingData{ declaration = id } }
|
||||
if lookup(scope.parent, name) |_| {
|
||||
if lookup(state, scope.parent, name) |_| {
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span)
|
||||
# poison the inner name rather than silently binding uses to its ancestor.
|
||||
value = Binding{ kind = .invalid, data = BindingData{ diagnostic = diagnostic } }
|
||||
@@ -171,14 +365,19 @@ resolve_function proc(
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
tree :: state.tree
|
||||
function :: ast.function_decl(tree, id)
|
||||
# signatures cannot see parameters or body-local declarations.
|
||||
try resolve_expr(state, package_scope, function.return_type)
|
||||
# compile-time parameters are in scope for subsequent dependent annotations.
|
||||
signature := Scope{ names = hashmap.init(state.allocator), parent = package_scope }
|
||||
defer hashmap.deinit(&signature.names)
|
||||
for (0..ast.function_param_count(tree, id)) |i| {
|
||||
parameter :: ast.function_param(tree, id, i)
|
||||
try resolve_expr(state, package_scope, ast.param(tree, parameter).type)
|
||||
try resolve_expr(state, &signature, ast.param(tree, parameter).type)
|
||||
if (ast.param(tree, parameter).comptime) try predeclare(state, &signature, parameter, ast.param(tree, parameter).name)
|
||||
}
|
||||
try resolve_expr(state, &signature, function.return_type)
|
||||
scope := Scope{
|
||||
names = hashmap.init(state.allocator),
|
||||
parent = package_scope,
|
||||
}
|
||||
|
||||
scope := Scope{ names = hashmap.init(state.allocator), parent = package_scope }
|
||||
defer hashmap.deinit(&scope.names)
|
||||
for (0..ast.function_param_count(tree, id)) |i| {
|
||||
parameter :: ast.function_param(tree, id, i)
|
||||
@@ -189,7 +388,10 @@ resolve_function proc(
|
||||
|
||||
@hide:file
|
||||
resolve_arm proc(state @mut State, parent @Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
scope := Scope{ names = hashmap.init(state.allocator), parent = parent }
|
||||
scope := Scope{
|
||||
names = hashmap.init(state.allocator),
|
||||
parent = parent,
|
||||
}
|
||||
defer hashmap.deinit(&scope.names)
|
||||
try resolve_block(state, &scope, id)
|
||||
}
|
||||
@@ -199,9 +401,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
|
||||
tree :: state.tree
|
||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||
statement :: ast.block_statement(tree, id, i)
|
||||
if ast.stmt_kind(tree, statement) == .declaration {
|
||||
try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
|
||||
}
|
||||
if (ast.stmt_kind(tree, statement) == .declaration) try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
|
||||
}
|
||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||
statement :: ast.block_statement(tree, id, i)
|
||||
@@ -211,11 +411,17 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
|
||||
.return_stmt: {
|
||||
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
|
||||
}
|
||||
.break_stmt, .continue_stmt: {}
|
||||
.yield_stmt: try resolve_expr(state, scope, ast.yield_stmt(tree, statement).value?)
|
||||
.break_stmt, .continue_stmt, .unreachable_stmt: {}
|
||||
.block_stmt: try resolve_arm(state, scope, ast.statement_block(tree, statement))
|
||||
.defer_stmt, .errdefer_stmt: {
|
||||
cleanup :: ast.cleanup_stmt(tree, statement)
|
||||
try resolve_capture_arm(state, scope, cleanup.body, cleanup.capture)
|
||||
}
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(tree, statement)
|
||||
try resolve_expr(state, scope, conditional.condition)
|
||||
try resolve_arm(state, scope, conditional.then_block)
|
||||
try resolve_capture_arm(state, scope, conditional.then_block, conditional.capture)
|
||||
if (conditional.else_block) |other| try resolve_arm(state, scope, other)
|
||||
}
|
||||
.assignment_stmt: {
|
||||
@@ -226,19 +432,18 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
|
||||
.while_stmt: {
|
||||
loop :: ast.while_stmt(tree, statement)
|
||||
try resolve_expr(state, scope, loop.condition)
|
||||
try resolve_arm(state, scope, loop.body)
|
||||
try resolve_capture_arm(state, scope, loop.body, loop.capture)
|
||||
}
|
||||
.for_stmt: {
|
||||
loop :: ast.for_stmt(tree, statement)
|
||||
try resolve_expr(state, scope, loop.iterable)
|
||||
body_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
|
||||
body_scope := Scope{
|
||||
names = hashmap.init(state.allocator),
|
||||
parent = scope,
|
||||
}
|
||||
defer hashmap.deinit(&body_scope.names)
|
||||
if (loop.capture) |capture| {
|
||||
try predeclare(state, &body_scope, capture, ast.node_span(tree, capture))
|
||||
}
|
||||
if (loop.index) |index| {
|
||||
try predeclare(state, &body_scope, index, ast.node_span(tree, index))
|
||||
}
|
||||
if (loop.capture) |capture| try predeclare(state, &body_scope, capture, ast.node_span(tree, capture))
|
||||
if (loop.index) |index| try predeclare(state, &body_scope, index, ast.node_span(tree, index))
|
||||
try resolve_block(state, &body_scope, loop.body)
|
||||
}
|
||||
.invalid: resolve_invalid(state, statement)
|
||||
@@ -246,6 +451,14 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_capture_arm proc(state @mut State, scope @Scope, block ast.NodeId, capture ?ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
child := Scope{ names = hashmap.init(state.allocator), parent = scope }
|
||||
defer hashmap.deinit(&child.names)
|
||||
if (capture) |id| try predeclare(state, &child, id, ast.node_span(state.tree, id))
|
||||
try resolve_block(state, &child, block)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_statement_decl proc(
|
||||
state @mut State,
|
||||
@@ -253,10 +466,67 @@ resolve_statement_decl proc(
|
||||
id ast.NodeId,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
declaration :: ast.statement_decl(state.tree, id)
|
||||
if (ast.expr_kind(state.tree, declaration.initializer) == .alias_expr) state.aliases[usize(id)] = ast.type_operand(state.tree, declaration.initializer)
|
||||
if (declaration.type) |type| try resolve_expr(state, scope, type)
|
||||
try resolve_expr(state, scope, declaration.initializer)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
canonical_binding proc(state @State, original Binding) Binding {
|
||||
value := original
|
||||
for (0..(state.aliases.len)) |_| {
|
||||
if (value.kind != .declaration) break
|
||||
operand :: state.aliases[usize(value.data.declaration)]
|
||||
if (operand == maxval!(ast.NodeId)) break
|
||||
target :: state.bindings[usize(operand)]
|
||||
if (target.kind == .none) break
|
||||
if (target.kind == .declaration and target.data.declaration == value.data.declaration) break
|
||||
value = target
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@hide:file
|
||||
namespace_member proc(state @State, root, name ast.NodeId) ?Binding {
|
||||
if (ast.expr_kind(state.tree, name) != .identifier) return null
|
||||
namespace :: hashmap.get(&state.namespaces, root)?
|
||||
if (hashmap.get(&namespace.names, ast.name_id(state.tree, name))) |member| {
|
||||
if (member.kind == .declaration and ast.package_local(state.tree, member.data.declaration)) return null
|
||||
return member
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@hide:file
|
||||
canonicalize_aliases proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
# aliases may forward-reference other aliases or imported namespace members.
|
||||
for (0..(state.aliases.len)) |_| {
|
||||
changed := false
|
||||
for (state.bindings) |@entry| {
|
||||
value :: canonical_binding(state, entry^)
|
||||
if (value.kind != entry.kind) changed = true
|
||||
else if (value.kind == .declaration and value.data.declaration != entry.data.declaration) changed = true
|
||||
entry^ = value
|
||||
}
|
||||
for (state.selections.items) |id| {
|
||||
expression :: ast.selection_expr(state.tree, id)
|
||||
base :: state.bindings[usize(expression.base)]
|
||||
if (base.kind != .namespace or state.bindings[usize(id)].kind != .none) continue
|
||||
found :: namespace_member(state, base.data.namespace, expression.member)
|
||||
value :: if (found) |member| canonical_binding(state, member) else Binding{
|
||||
kind = .invalid,
|
||||
data = BindingData{ diagnostic = try diagnostics.add(
|
||||
state.diagnostics, .unknown_name, ast.node_span(state.tree, expression.member),
|
||||
) },
|
||||
}
|
||||
state.bindings[usize(id)] = value
|
||||
state.bindings[usize(expression.member)] = value
|
||||
changed = true
|
||||
}
|
||||
if (!changed) break
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_invalid proc(state @mut State, id ast.NodeId) void {
|
||||
if (state.bindings[usize(id)].kind != .none) return
|
||||
@@ -276,11 +546,63 @@ resolve_expr proc(
|
||||
if (state.bindings[usize(id)].kind != .none) return
|
||||
tree :: state.tree
|
||||
match ast.expr_kind(tree, id) {
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string: {}
|
||||
.comptime_expr: try resolve_expr(state, scope, ast.comptime_operand(tree, id))
|
||||
.block_expr: try resolve_arm(state, scope, ast.block_expr(tree, id))
|
||||
.typed_constant: {
|
||||
value :: ast.typed_constant(tree, id)
|
||||
try resolve_expr(state, scope, value.type)
|
||||
try resolve_expr(state, scope, value.value)
|
||||
}
|
||||
.union_constant: {
|
||||
value :: ast.union_constant(tree, id)
|
||||
try resolve_expr(state, scope, value.type)
|
||||
try resolve_expr(state, scope, value.value)
|
||||
}
|
||||
.slice_constant: {
|
||||
value :: ast.slice_constant(tree, id)
|
||||
try resolve_expr(state, scope, value.type)
|
||||
try resolve_expr(state, scope, value.value)
|
||||
}
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void,
|
||||
.literal_undefined: {}
|
||||
.enum_literal: {}
|
||||
.distinct_type, .alias_expr, .union_tag_type, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
|
||||
.enum_type: {
|
||||
if (ast.enum_type(tree, id).backing) |backing| try resolve_expr(state, scope, backing)
|
||||
for (0..ast.enum_member_count(tree, id)) |i| {
|
||||
member :: ast.field(tree, ast.enum_member(tree, id, i))
|
||||
if (member.value) |value| try resolve_expr(state, scope, value)
|
||||
}
|
||||
}
|
||||
.match_expr: {
|
||||
try resolve_expr(state, scope, ast.match_expr(tree, id).subject)
|
||||
for (0..ast.match_arm_count(tree, id)) |i| {
|
||||
arm_id :: ast.match_arm(tree, id, i)
|
||||
arm :: ast.match_arm_view(tree, arm_id)
|
||||
for (0..ast.match_pattern_count(tree, arm_id)) |j| {
|
||||
try resolve_expr(state, scope, ast.match_pattern(tree, arm_id, j))
|
||||
}
|
||||
arm_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
|
||||
defer hashmap.deinit(&arm_scope.names)
|
||||
if (arm.capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture))
|
||||
if (arm.tag_capture) |capture| try predeclare(state, &arm_scope, capture, ast.node_span(tree, capture))
|
||||
if (arm.expression) try resolve_expr(state, &arm_scope, arm.body)
|
||||
else try resolve_block(state, &arm_scope, arm.body)
|
||||
}
|
||||
}
|
||||
.catch_expr: {
|
||||
expression :: ast.catch_expr(tree, id)
|
||||
try resolve_expr(state, scope, expression.operand)
|
||||
handler_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
|
||||
defer hashmap.deinit(&handler_scope.names)
|
||||
if (expression.capture) |capture| try predeclare(state, &handler_scope, capture, ast.node_span(tree, capture))
|
||||
if (expression.handler_is_block) try resolve_block(state, &handler_scope, expression.handler)
|
||||
else try resolve_expr(state, &handler_scope, expression.handler)
|
||||
}
|
||||
.identifier: {
|
||||
span :: ast.node_span(tree, id)
|
||||
name :: ast.source_text(tree, span)
|
||||
if lookup(scope, name) |found| {
|
||||
name :: ast.name_id(tree, id)
|
||||
if lookup(state, scope, name) |found| {
|
||||
state.bindings[usize(id)] = found
|
||||
} else {
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unknown_name, span)
|
||||
@@ -290,8 +612,33 @@ resolve_expr proc(
|
||||
}
|
||||
}
|
||||
}
|
||||
.selection: {
|
||||
expression :: ast.selection_expr(tree, id)
|
||||
try arraylist.append(&state.selections, id)
|
||||
try resolve_expr(state, scope, expression.base)
|
||||
base :: state.bindings[usize(expression.base)]
|
||||
value := Binding{}
|
||||
if (base.kind == .invalid) {
|
||||
value = base
|
||||
} else if (base.kind == .namespace) {
|
||||
found :: namespace_member(state, base.data.namespace, expression.member)
|
||||
if (found) |member| {
|
||||
value = member
|
||||
} else {
|
||||
value = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(
|
||||
state.diagnostics,
|
||||
.unknown_name,
|
||||
ast.node_span(tree, expression.member),
|
||||
),
|
||||
} }
|
||||
}
|
||||
}
|
||||
state.bindings[usize(id)] = value
|
||||
state.bindings[usize(expression.member)] = value
|
||||
}
|
||||
.unary: try resolve_expr(state, scope, ast.unary_expr(tree, id).operand)
|
||||
.binary: {
|
||||
.binary, .fallible_type, .sum_type: {
|
||||
expression :: ast.binary_expr(tree, id)
|
||||
try resolve_expr(state, scope, expression.left)
|
||||
try resolve_expr(state, scope, expression.right)
|
||||
@@ -307,6 +654,29 @@ resolve_expr proc(
|
||||
try resolve_expr(state, scope, ast.call_arg(tree, id, i))
|
||||
}
|
||||
}
|
||||
.builtin_call: {
|
||||
for (0..ast.call_arg_count(tree, id)) |i| try resolve_expr(state, scope, ast.call_arg(tree, id, i))
|
||||
}
|
||||
.slice: {
|
||||
expression :: ast.slice_expr(tree, id)
|
||||
try resolve_expr(state, scope, expression.base)
|
||||
if (expression.start) |node| try resolve_expr(state, scope, node)
|
||||
if (expression.end) |node| try resolve_expr(state, scope, node)
|
||||
if (expression.sentinel) |node| try resolve_expr(state, scope, node)
|
||||
}
|
||||
.slice_type: {
|
||||
type :: ast.slice_type(tree, id)
|
||||
try resolve_expr(state, scope, type.element)
|
||||
if (type.sentinel) |node| try resolve_expr(state, scope, node)
|
||||
}
|
||||
.pointer_type: try resolve_expr(state, scope, ast.pointer_type(tree, id).element)
|
||||
.function_type: {
|
||||
try resolve_expr(state, scope, ast.callable_result(tree, id))
|
||||
for 0..ast.function_param_count(tree, id) |i| {
|
||||
parameter :: ast.function_param(tree, id, i)
|
||||
try resolve_expr(state, scope, ast.param(tree, parameter).type)
|
||||
}
|
||||
}
|
||||
.array_type: {
|
||||
type :: ast.array_type(tree, id)
|
||||
if (type.count) |count| try resolve_expr(state, scope, count)
|
||||
@@ -317,6 +687,19 @@ resolve_expr proc(
|
||||
try resolve_expr(state, scope, ast.array_element(tree, id, i))
|
||||
}
|
||||
}
|
||||
.record_type, .record_literal, .union_type: {
|
||||
if (ast.expr_kind(tree, id) == .record_literal) {
|
||||
if (ast.record_literal(tree, id).type) |type| try resolve_expr(state, scope, type)
|
||||
}
|
||||
if (ast.expr_kind(tree, id) == .union_type) {
|
||||
if (ast.union_type(tree, id).tag) |tag| try resolve_expr(state, scope, tag)
|
||||
}
|
||||
for (0..ast.record_field_count(tree, id)) |i| {
|
||||
field :: ast.field(tree, ast.record_field(tree, id, i))
|
||||
if (field.type) |type| try resolve_expr(state, scope, type)
|
||||
if (field.value) |value| try resolve_expr(state, scope, value)
|
||||
}
|
||||
}
|
||||
.index: {
|
||||
expression :: ast.index_expr(tree, id)
|
||||
try resolve_expr(state, scope, expression.base)
|
||||
@@ -325,3 +708,4 @@ resolve_expr proc(
|
||||
.invalid: resolve_invalid(state, id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+265
-48
@@ -8,8 +8,8 @@ import "@source/parser"
|
||||
import "@source/ast"
|
||||
|
||||
resolves_forward_references_recursion_and_builtin_signatures test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -25,10 +25,10 @@ resolves_forward_references_recursion_and_builtin_signatures test {
|
||||
`later int :: 1
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
|
||||
first :: ast.package_decl(&tree, 0)
|
||||
@@ -110,8 +110,8 @@ resolves_forward_references_recursion_and_builtin_signatures test {
|
||||
}
|
||||
|
||||
rejects_shadowing_and_duplicates_without_cascades test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -132,11 +132,11 @@ rejects_shadowing_and_duplicates_without_cascades test {
|
||||
`same :: same
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
|
||||
function :: ast.package_decl(&tree, 0)
|
||||
@@ -228,8 +228,8 @@ rejects_shadowing_and_duplicates_without_cascades test {
|
||||
}
|
||||
|
||||
preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -243,7 +243,7 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
syntax_count :: diagnostics.count(&diagnostic_store)
|
||||
@@ -255,7 +255,7 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
|
||||
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, empty))
|
||||
invalid_statement :: ast.block_statement(&tree, view.body, 4)
|
||||
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, invalid_statement))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
|
||||
invalid_nodes [3]ast.NodeId :: [invalid_decl, empty, invalid_statement]
|
||||
@@ -300,8 +300,8 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
|
||||
}
|
||||
|
||||
resolves_conditional_arm_scopes_without_leaks_or_shadowing test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -323,11 +323,11 @@ resolves_conditional_arm_scopes_without_leaks_or_shadowing test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
|
||||
function :: ast.package_decl(&tree, 0)
|
||||
@@ -368,8 +368,8 @@ resolves_conditional_arm_scopes_without_leaks_or_shadowing test {
|
||||
}
|
||||
|
||||
resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -394,11 +394,11 @@ resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
|
||||
function :: ast.package_decl(&tree, 0)
|
||||
@@ -441,8 +441,8 @@ resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings test {
|
||||
}
|
||||
|
||||
resolves_all_code_and_preserves_scopes_after_loop_controls test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -464,11 +464,11 @@ resolves_all_code_and_preserves_scopes_after_loop_controls test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
@@ -487,8 +487,8 @@ resolves_all_code_and_preserves_scopes_after_loop_controls test {
|
||||
}
|
||||
|
||||
range_bounds_use_outer_scope_and_captures_are_body_declarations test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -506,10 +506,10 @@ range_bounds_use_outer_scope_and_captures_are_body_declarations test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||
@@ -540,8 +540,8 @@ range_bounds_use_outer_scope_and_captures_are_body_declarations test {
|
||||
}
|
||||
|
||||
range_captures_preserve_duplicate_builtin_and_discard_errors test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -563,10 +563,10 @@ range_captures_preserve_duplicate_builtin_and_discard_errors test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
for (usize(1)..usize(3)) |index| {
|
||||
@@ -581,9 +581,8 @@ range_captures_preserve_duplicate_builtin_and_discard_errors test {
|
||||
} else return .expectation_failed
|
||||
}
|
||||
indexed :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||
if (indexed.index) |index| {
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, index).kind)
|
||||
} else return .expectation_failed
|
||||
if (indexed.index) |index| try testing.expect_equal(BindingKind.invalid, binding(&resolution, index).kind)
|
||||
else return .expectation_failed
|
||||
if (indexed.capture) |capture| {
|
||||
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, indexed.body, 0))
|
||||
try testing.expect_equal(capture, binding(&resolution, reference).data.declaration)
|
||||
@@ -605,8 +604,8 @@ range_captures_preserve_duplicate_builtin_and_discard_errors test {
|
||||
}
|
||||
|
||||
resolves_array_counts_elements_indices_and_unreachable_targets test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -621,10 +620,10 @@ resolves_array_counts_elements_indices_and_unreachable_targets test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
size :: ast.package_decl(&tree, 0)
|
||||
@@ -669,8 +668,8 @@ resolves_array_counts_elements_indices_and_unreachable_targets test {
|
||||
}
|
||||
|
||||
array_signature_counts_use_package_scope_and_local_counts_see_parameters test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -682,11 +681,11 @@ array_signature_counts_use_package_scope_and_local_counts_see_parameters test {
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
function :: ast.package_decl(&tree, 1)
|
||||
@@ -710,8 +709,8 @@ array_signature_counts_use_package_scope_and_local_counts_see_parameters test {
|
||||
}
|
||||
|
||||
resolves_every_array_child_error_and_preserves_lexer_diagnostics test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
@@ -719,15 +718,15 @@ resolves_every_array_child_error_and_preserves_lexer_diagnostics test {
|
||||
` return
|
||||
` data [missing_count]MissingElement :: [absent, missing_element]
|
||||
` (missing_base[missing_index])[missing_outer] = missing_rhs
|
||||
` malformed [$]i64 :: [$]
|
||||
` malformed [%]i64 :: [%]
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(10, diagnostics.count(&diagnostic_store))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
@@ -755,3 +754,221 @@ resolves_every_array_child_error_and_preserves_lexer_diagnostics test {
|
||||
try testing.expect_equal(diagnostics.DiagnosticId(0), binding(&resolution, malformed_count).data.diagnostic)
|
||||
try testing.expect_equal(diagnostics.DiagnosticId(1), binding(&resolution, malformed_element).data.diagnostic)
|
||||
}
|
||||
|
||||
resolves_interleaved_compilations_with_independent_identifier_orders test {
|
||||
first_pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&first_pool)
|
||||
second_pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&second_pool)
|
||||
first_diagnostics := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&first_diagnostics)
|
||||
second_diagnostics := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&second_diagnostics)
|
||||
first_source :: "alpha :: beta\nbeta i64 :: 1\nmain proc() bool { return true }\n"
|
||||
second_source :: "beta :: alpha\nalpha bool :: false\nmain proc() i64 { return 1 }\n"
|
||||
first_scan := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&first_scan)
|
||||
second_scan := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&second_scan)
|
||||
try lexer.scan(&first_scan, first_source, &first_pool, &first_diagnostics)
|
||||
first_tree := try parser.parse(mem.c_allocator, first_scan.tokens.items, first_source, &first_diagnostics)
|
||||
defer ast.deinit(&first_tree)
|
||||
try lexer.scan(&second_scan, second_source, &second_pool, &second_diagnostics)
|
||||
second_tree := try parser.parse(mem.c_allocator, second_scan.tokens.items, second_source, &second_diagnostics)
|
||||
defer ast.deinit(&second_tree)
|
||||
first_name :: ast.name_id(&first_tree, ast.package_decl(&first_tree, 0))
|
||||
second_name :: ast.name_id(&second_tree, ast.package_decl(&second_tree, 0))
|
||||
try testing.expect_equal(first_name, second_name)
|
||||
try testing.expect_equal("alpha", strpool.get_str(&first_pool, first_name)?)
|
||||
try testing.expect_equal("beta", strpool.get_str(&second_pool, second_name)?)
|
||||
first_resolution := try resolve(mem.c_allocator, &first_tree, &first_pool, &first_diagnostics)
|
||||
defer deinit(&first_resolution)
|
||||
second_resolution := try resolve(mem.c_allocator, &second_tree, &second_pool, &second_diagnostics)
|
||||
defer deinit(&second_resolution)
|
||||
first_decl :: ast.package_decl(&first_tree, 0)
|
||||
second_decl :: ast.package_decl(&second_tree, 0)
|
||||
first_target :: ast.package_decl(&first_tree, 1)
|
||||
second_target :: ast.package_decl(&second_tree, 1)
|
||||
first_use :: ast.statement_decl(&first_tree, first_decl).initializer
|
||||
second_use :: ast.statement_decl(&second_tree, second_decl).initializer
|
||||
try testing.expect_equal(BindingKind.declaration, binding(&first_resolution, first_use).kind)
|
||||
try testing.expect_equal(first_target, binding(&first_resolution, first_use).data.declaration)
|
||||
try testing.expect_equal(BindingKind.declaration, binding(&second_resolution, second_use).kind)
|
||||
try testing.expect_equal(second_target, binding(&second_resolution, second_use).data.declaration)
|
||||
first_type :: ast.statement_decl(&first_tree, first_target).type?
|
||||
second_type :: ast.statement_decl(&second_tree, second_target).type?
|
||||
try testing.expect_equal(BindingKind.builtin, binding(&first_resolution, first_type).kind)
|
||||
try testing.expect_equal(BuiltinType.i64, binding(&first_resolution, first_type).data.builtin)
|
||||
try testing.expect_equal(BindingKind.builtin, binding(&second_resolution, second_type).kind)
|
||||
try testing.expect_equal(BuiltinType.bool, binding(&second_resolution, second_type).data.builtin)
|
||||
first_return :: ast.function_decl(&first_tree, ast.package_decl(&first_tree, 2)).return_type
|
||||
second_return :: ast.function_decl(&second_tree, ast.package_decl(&second_tree, 2)).return_type
|
||||
try testing.expect_equal(BuiltinType.bool, binding(&first_resolution, first_return).data.builtin)
|
||||
try testing.expect_equal(BuiltinType.i64, binding(&second_resolution, second_return).data.builtin)
|
||||
try testing.expect_equal(usize(0), diagnostics.count(&first_diagnostics))
|
||||
try testing.expect_equal(usize(0), diagnostics.count(&second_diagnostics))
|
||||
}
|
||||
|
||||
resolves_match_capture_scopes_without_looking_up_member_names test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`Tag :: enum { if, else }
|
||||
`Value :: union(Tag) { if i32, else void }
|
||||
`inspect proc(value Value) void {
|
||||
` match value {
|
||||
` .if |payload|: { local :: payload }
|
||||
` .else: { local :: 1 }
|
||||
` }
|
||||
` payload
|
||||
` local
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
|
||||
matched :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0))
|
||||
first :: ast.match_arm(&tree, matched, 0)
|
||||
arm :: ast.match_arm_view(&tree, first)
|
||||
local :: ast.statement_decl(&tree, ast.block_statement(&tree, arm.body, 0))
|
||||
try testing.expect_equal(arm.capture?, binding(&resolution, local.initializer).data.declaration)
|
||||
member :: ast.enum_literal(&tree, ast.match_pattern(&tree, first, 0))
|
||||
try testing.expect_equal(BindingKind.none, binding(&resolution, member).kind)
|
||||
for (usize(1)..usize(3)) |index| {
|
||||
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, body, index))
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, reference).kind)
|
||||
}
|
||||
}
|
||||
|
||||
canonicalizes_forward_function_global_and_builtin_aliases test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`invoke :: alias call
|
||||
`copy :: alias storage
|
||||
`Number :: alias i32
|
||||
`call :: alias original
|
||||
`original proc() i32 { return storage }
|
||||
`storage i32 := 1
|
||||
`main proc() Number {
|
||||
` copy = invoke()
|
||||
` return copy
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
original :: ast.package_decl(&tree, 4)
|
||||
storage :: ast.package_decl(&tree, 5)
|
||||
main :: ast.function_decl(&tree, ast.package_decl(&tree, 6))
|
||||
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, main.body, 0))
|
||||
try testing.expect_equal(storage, binding(&resolution, assignment.target).data.declaration)
|
||||
try testing.expect_equal(original, binding(&resolution, ast.call_expr(&tree, assignment.value).callee).data.declaration)
|
||||
try testing.expect_equal(BindingKind.builtin, binding(&resolution, main.return_type).kind)
|
||||
try testing.expect_equal(BuiltinType.i32, binding(&resolution, main.return_type).data.builtin)
|
||||
try testing.expect_equal(original, binding(&resolution, ast.package_decl(&tree, 0)).data.declaration)
|
||||
}
|
||||
|
||||
canonicalizes_imported_aliases_and_namespace_alias_selections test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
builder := ast.init_builder(mem.c_allocator, "")
|
||||
defer ast.deinit_builder(&builder)
|
||||
library_source :: "original proc() i32 { return storage }\nstorage i32 := 1\nexported :: alias original\n"
|
||||
library_id :: try ast.set_source(&builder, "library.hon", library_source)
|
||||
library_scan := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&library_scan)
|
||||
library_scan.source_id = library_id
|
||||
try lexer.scan(&library_scan, library_source, &pool, &diagnostic_store)
|
||||
library :: try parser.parse_source(&builder, library_scan.tokens.items, &diagnostic_store)
|
||||
main_source ::
|
||||
`lib :: import "library"
|
||||
`invoke :: alias namespace.exported
|
||||
`copy :: alias namespace.storage
|
||||
`namespace :: alias lib
|
||||
`main proc() i32 { copy = invoke()
|
||||
` return copy
|
||||
`}
|
||||
main_id :: try ast.set_source(&builder, "main.hon", main_source)
|
||||
main_scan := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&main_scan)
|
||||
main_scan.source_id = main_id
|
||||
try lexer.scan(&main_scan, main_source, &pool, &diagnostic_store)
|
||||
root :: try parser.parse_source(&builder, main_scan.tokens.items, &diagnostic_store)
|
||||
tree := ast.finish(&builder, root)
|
||||
defer ast.deinit(&tree)
|
||||
roots [2]ast.NodeId :: [root, library]
|
||||
imports [1]Import :: [Import{
|
||||
node = ast.package_decl_at(&tree, root, 0),
|
||||
name = strpool.get_id(&pool, "lib")?,
|
||||
target = library,
|
||||
}]
|
||||
resolution := try resolve_packages(mem.c_allocator, &tree, roots[..], imports[..], &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
original :: ast.package_decl_at(&tree, library, 0)
|
||||
storage :: ast.package_decl_at(&tree, library, 1)
|
||||
main :: ast.function_decl(&tree, ast.package_decl_at(&tree, root, 4))
|
||||
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, main.body, 0))
|
||||
try testing.expect_equal(storage, binding(&resolution, assignment.target).data.declaration)
|
||||
try testing.expect_equal(original, binding(&resolution, ast.call_expr(&tree, assignment.value).callee).data.declaration)
|
||||
namespace :: ast.package_decl_at(&tree, root, 3)
|
||||
try testing.expect_equal(BindingKind.namespace, binding(&resolution, namespace).kind)
|
||||
}
|
||||
|
||||
resolves_inline_match_captures_and_all_bodies test {
|
||||
pool := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&pool)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`inspect proc(value int) void {
|
||||
` match value { inline |payload, tag|: {
|
||||
` payload
|
||||
` tag
|
||||
` } }
|
||||
` match value { inline |same, same|: same }
|
||||
` match value { inline |payload, value|: value }
|
||||
` match value { .if: 1, inline |_|: missing }
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &pool, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &pool, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
first_match :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0))
|
||||
first :: ast.match_arm_view(&tree, ast.match_arm(&tree, first_match, 0))
|
||||
payload_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, first.body, 0))
|
||||
tag_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, first.body, 1))
|
||||
try testing.expect_equal(first.capture?, binding(&resolution, payload_ref).data.declaration)
|
||||
try testing.expect_equal(first.tag_capture?, binding(&resolution, tag_ref).data.declaration)
|
||||
for (usize(1)..usize(3)) |i| {
|
||||
matched :: ast.statement_expr(&tree, ast.block_statement(&tree, body, i))
|
||||
arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, matched, 0))
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, arm.tag_capture?).kind)
|
||||
}
|
||||
last_match :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 3))
|
||||
last :: ast.match_arm_view(&tree, ast.match_arm(&tree, last_match, 1))
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, last.body).kind)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,506 @@
|
||||
import "@std/mem"
|
||||
import "@source/ast"
|
||||
import "@source/diagnostics"
|
||||
import "@source/strpool"
|
||||
sema :: import "@source/sema"
|
||||
typecheck :: import "@source/sema/check"
|
||||
import "@std/testing"
|
||||
import "@source/lexer"
|
||||
import "@source/parser"
|
||||
|
||||
@hide:file
|
||||
expect_specialization proc(
|
||||
source []u8, expected diagnostics.Code, expected_count usize,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError | testing.Error) {
|
||||
try expect_specialization_guards(source, expected, expected_count, maxval!(usize))
|
||||
}
|
||||
|
||||
@hide:file
|
||||
expect_specialization_guards proc(
|
||||
source []u8, expected diagnostics.Code, expected_count, expected_guards usize,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError | testing.Error) {
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
|
||||
builder := ast.init_builder(mem.c_allocator, source)
|
||||
defer ast.deinit_builder(&builder)
|
||||
root :: try parser.parse_source(&builder, scan_state.tokens.items, &diagnostic_store)
|
||||
tree := ast.view(&builder, root)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &strings, &diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
elaborated := try elaborate(mem.c_allocator, &builder, root, &resolution, &strings, &diagnostic_store)
|
||||
defer deinit(&elaborated)
|
||||
tree = ast.view(&builder, root)
|
||||
checked := try typecheck.check_elaborated(mem.c_allocator, &tree, &resolution, elaborated.constraints.items, &diagnostic_store)
|
||||
defer typecheck.deinit(&checked)
|
||||
if (expected_count == 0) {
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
for (0..ast.package_decl_count(&tree)) |i| {
|
||||
declaration :: ast.package_decl(&tree, i)
|
||||
if (ast.node_class(&tree, declaration) != .function) continue
|
||||
try testing.expect(!ast.function_generic(&tree, declaration))
|
||||
for (0..ast.function_param_count(&tree, declaration)) |j|
|
||||
try testing.expect(!ast.param(&tree, ast.function_param(&tree, declaration, j)).comptime)
|
||||
}
|
||||
if (expected_guards != maxval!(usize))
|
||||
try testing.expect_equal(expected_guards, runtime_guard_count(&tree, root))
|
||||
} else {
|
||||
count := usize(0)
|
||||
for (diagnostic_store.list.items) |diagnostic| {
|
||||
if (diagnostic.code == expected) count += 1
|
||||
}
|
||||
if (expected_count == maxval!(usize)) try testing.expect(count > 0)
|
||||
else try testing.expect_equal(expected_count, count)
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
runtime_guard_count proc(tree @ast.Tree, id ast.NodeId) usize {
|
||||
count := usize(0)
|
||||
if (ast.node_class(tree, id) == .statement and ast.stmt_kind(tree, id) == .if_stmt) count += 1
|
||||
for (0..ast.child_count(tree, id)) |i| count += runtime_guard_count(tree, ast.child(tree, id, i))
|
||||
return count
|
||||
}
|
||||
|
||||
selects_known_type_helper_guards_without_explicit_comptime test {
|
||||
try expect_specialization_guards(
|
||||
`Id :: distinct i16
|
||||
`primitive proc($T type) bool { return T == i16 or T == Id }
|
||||
`scalar proc($T type) bool { return primitive(T) }
|
||||
`accept proc($T type, value T) T {
|
||||
` if scalar(T) { return value }
|
||||
` else { compile_error!("expected scalar") }
|
||||
`}
|
||||
`main proc() i16 { return accept(i16(42)) }
|
||||
, .compile_error, 0, 0,
|
||||
)
|
||||
try expect_specialization(
|
||||
`scalar proc($T type) bool { return T == i16 }
|
||||
`accept proc($T type, value T) T {
|
||||
` if scalar(T) { return value }
|
||||
` else { compile_error!("expected scalar") }
|
||||
`}
|
||||
`main proc() bool { return accept(true) }
|
||||
, .compile_error, 1,
|
||||
)
|
||||
}
|
||||
|
||||
selects_matched_type_helper_guards_before_expanding_invalid_arms test {
|
||||
try expect_specialization_guards(
|
||||
`scalar proc($T type) bool {
|
||||
` match T == i16 {
|
||||
` true: return true
|
||||
` else: return false
|
||||
` }
|
||||
`}
|
||||
`accept proc($T type, value T) T {
|
||||
` if scalar(T) { return value }
|
||||
` else { compile_error!("expected scalar") }
|
||||
`}
|
||||
`main proc() i16 { return accept(i16(42)) }
|
||||
, .compile_error, 0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
preserves_runtime_guards_with_constant_arguments_or_type_dependent_effects test {
|
||||
sources [6][]u8 :: [
|
||||
`advance proc(value @mut i16) void { value^ += 1 }
|
||||
`predicate proc(value i16) bool {
|
||||
` local i16 := value
|
||||
` advance(&local)
|
||||
` return local == 42
|
||||
`}
|
||||
`choose proc($T type, value T) T {
|
||||
` if predicate(42) { return value } else { return value }
|
||||
`}
|
||||
`main proc() i16 { return choose(i16(42)) }
|
||||
,
|
||||
`advance proc(value @mut i16) void { value^ += 1 }
|
||||
`predicate proc($T type) bool {
|
||||
` local i16 := 41
|
||||
` advance(&local)
|
||||
` return T == i16 and local == 42
|
||||
`}
|
||||
`choose proc($T type, value T) T {
|
||||
` if predicate(T) { return value } else { return value }
|
||||
`}
|
||||
`main proc() i16 { return choose(i16(42)) }
|
||||
,
|
||||
`enabled := true
|
||||
`predicate proc($T type) bool { return enabled }
|
||||
`choose proc($T type, value T) T {
|
||||
` if predicate(T) { return value } else { return value }
|
||||
`}
|
||||
`main proc() i16 { return choose(i16(42)) }
|
||||
,
|
||||
`observe c_func() bool
|
||||
`predicate proc($T type) bool { return observe() }
|
||||
`choose proc($T type, value T) T {
|
||||
` if predicate(T) { return value } else { return value }
|
||||
`}
|
||||
`main proc() i16 { return choose(i16(42)) }
|
||||
,
|
||||
`predicate proc($T type) bool { unreachable }
|
||||
`choose proc($T type, value T) T {
|
||||
` if predicate(T) { return value } else { return value }
|
||||
`}
|
||||
`main proc() i16 { return choose(i16(42)) }
|
||||
,
|
||||
`predicate proc($T type, flag bool) bool { return flag }
|
||||
`choose proc($T type, value T) T {
|
||||
` if predicate(T, true) { return value } else { return value }
|
||||
`}
|
||||
`main proc() i16 { return choose(i16(42)) }
|
||||
,
|
||||
]
|
||||
for (sources) |source|
|
||||
try expect_specialization_guards(source, .compile_error, 0, 1)
|
||||
}
|
||||
|
||||
demands_inline_iterable_calls_before_expanding_their_bodies test {
|
||||
try expect_specialization(
|
||||
`fill proc(values []mut i16, value i16) void {
|
||||
` for (0..values.len) |i| values[i] = value
|
||||
`}
|
||||
`produce proc($N usize, $seed i16) [N]mut i16 {
|
||||
` values [N]mut i16 := undefined
|
||||
` fill(values[..], seed)
|
||||
` if values[0] != seed { compile_error!("unexpected initialization") }
|
||||
` return values
|
||||
`}
|
||||
`main proc() i16 {
|
||||
` total i16 := 0
|
||||
` inline for (produce(3, 14)) |value| total += value
|
||||
` return total
|
||||
`}
|
||||
, .compile_error, 0,
|
||||
)
|
||||
try expect_specialization(
|
||||
`produce proc($N usize) [N]i16 {
|
||||
` if N == 2 { compile_error!("reached failure") }
|
||||
` return [1, 2]
|
||||
`}
|
||||
`main proc() void { inline for (produce(2)) |value| {} }
|
||||
, .compile_error, 1,
|
||||
)
|
||||
}
|
||||
|
||||
explicit_demand_executes_ordinary_calls_and_preserves_borrow_aliases test {
|
||||
try expect_specialization(
|
||||
`update proc(left, right []mut i16) void {
|
||||
` left[0] = 19
|
||||
` right[1] = left[0] + 4
|
||||
`}
|
||||
`produce proc() [2]mut i16 {
|
||||
` values [2]mut i16 := undefined
|
||||
` update(values[..], values[..])
|
||||
` if values[1] != 23 { compile_error!("lost mutable alias") }
|
||||
` return values
|
||||
`}
|
||||
`main proc() i16 { values :: $(produce())
|
||||
` return values[0] + values[1]
|
||||
`}
|
||||
, .compile_error, 0,
|
||||
)
|
||||
}
|
||||
|
||||
demanded_calls_still_infer_omitted_compiletime_type_arguments test {
|
||||
try expect_specialization(
|
||||
`identity proc($T type, value T) T { return value }
|
||||
`main proc() i16 { return $(identity(i16(42))) }
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
demanded_expression_inline_matches_bind_payloads_and_static_tags test {
|
||||
try expect_specialization(
|
||||
`Detail :: union(enum) { count usize, empty void }
|
||||
`score proc(input Detail) i16 {
|
||||
` return match input {
|
||||
` inline |payload, tag|: match tag { .count: i16(payload), else: 0 },
|
||||
` }
|
||||
`}
|
||||
`main proc() i16 { return $(score(.count{42})) }
|
||||
, .invalid_match_capture, 0,
|
||||
)
|
||||
}
|
||||
|
||||
keeps_borrowed_snapshots_owned_and_explicit_numeric_annotations_rigid test {
|
||||
try expect_specialization(
|
||||
`borrow proc(values []mut i16) []mut i16 { return values }
|
||||
`produce proc() []mut i16 {
|
||||
` values [2]mut i16 := [19, 23]
|
||||
` return borrow(values[..])
|
||||
`}
|
||||
`main proc() void { values :: $(produce()) }
|
||||
, .comptime_value_escape, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`produce proc() usize {
|
||||
` value i16 := 1
|
||||
` if value == 0 { compile_error!("unselected typed guard") }
|
||||
` return value
|
||||
`}
|
||||
`main proc() void { value :: $(produce()) }
|
||||
, .type_mismatch, 1,
|
||||
)
|
||||
}
|
||||
|
||||
keeps_inferred_compiletime_literals_contextual_until_a_rigid_source test {
|
||||
try expect_specialization(
|
||||
`produce proc() usize {
|
||||
` width := 2
|
||||
` width = 3
|
||||
` cursor := 0
|
||||
` cursor += width
|
||||
` values [4]i16 :: [1, 2, 3, 42]
|
||||
` if values[cursor] != 42 { compile_error!("wrong contextual index") }
|
||||
` count := 0
|
||||
` count = values.len
|
||||
` return count
|
||||
`}
|
||||
`main proc() usize { return $(produce()) }
|
||||
, .compile_error, 0,
|
||||
)
|
||||
try expect_specialization(
|
||||
`produce proc() usize {
|
||||
` width i16 := 2
|
||||
` cursor usize := 0
|
||||
` if cursor == 99 { compile_error!("unselected typed guard") }
|
||||
` cursor += width
|
||||
` return cursor
|
||||
`}
|
||||
`main proc() usize { return $(produce()) }
|
||||
, .invalid_operand, maxval!(usize),
|
||||
)
|
||||
}
|
||||
|
||||
demanded_local_execution_retains_overflow_and_initialization_diagnostics test {
|
||||
try expect_specialization(
|
||||
`produce proc() u8 {
|
||||
` value u8 := 255
|
||||
` value += 1
|
||||
` return value
|
||||
`}
|
||||
`main proc() u8 { return $(produce()) }
|
||||
, .integer_overflow, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`initialize proc(values []mut i16) void { values[0] = 42 }
|
||||
`produce proc() i16 {
|
||||
` values [2]mut i16 := undefined
|
||||
` initialize(values[..])
|
||||
` return values[1]
|
||||
`}
|
||||
`main proc() i16 { return $(produce()) }
|
||||
, .read_before_initialization, 1,
|
||||
)
|
||||
}
|
||||
|
||||
infers_nested_nominal_pointer_parameters_and_readonly_slice_views test {
|
||||
try expect_specialization(
|
||||
`Storage proc($T type) type { return struct { items []mut T, capacity usize } }
|
||||
`make proc($T type, values []mut T) Storage(T) {
|
||||
` return Storage(T) { items = values, capacity = values.len }
|
||||
`}
|
||||
`observe proc($T type, values []T) usize { return values.len }
|
||||
`reserve proc($T type, storage @mut Storage(T), count usize) void {
|
||||
` observe(storage.items.ptr[..count])
|
||||
`}
|
||||
`append proc($T type, storage @mut Storage(T), value T) void {
|
||||
` reserve(storage, storage.items.len)
|
||||
` storage.items[0] = value
|
||||
`}
|
||||
`release proc($T type, storage @mut Storage(T)) void {
|
||||
` allocation :: storage.items.ptr[..storage.capacity]
|
||||
` observe(allocation)
|
||||
`}
|
||||
`forward proc(storage @mut Storage(i16)) void {
|
||||
` append(storage, 7)
|
||||
` release(storage)
|
||||
`}
|
||||
`main proc() i16 {
|
||||
` values [2]mut i16 := [1, 2]
|
||||
` storage := make(i16, values[..])
|
||||
` forward(&storage)
|
||||
` append(&storage, 9)
|
||||
` release(&storage)
|
||||
` return storage.items[0]
|
||||
`}
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
rejects_conflicting_rigid_types_and_unrelated_nominal_generators test {
|
||||
try expect_specialization(
|
||||
`same proc($T type, left, right T) void {}
|
||||
`main proc() void { same(i16(1), u8(2)) }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`Box proc($T type) type { return struct { value T } }
|
||||
`Other proc($T type) type { return struct { value T } }
|
||||
`read proc($T type, value @Box(T)) void {}
|
||||
`main proc() void {
|
||||
` value Other(i16) :: { value = 1 }
|
||||
` read(&value)
|
||||
`}
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`unknown proc($T type, value usize) void {}
|
||||
`main proc() void { unknown(1) }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
}
|
||||
|
||||
preserves_rigid_inference_before_contextual_literals test {
|
||||
try expect_specialization(
|
||||
`same proc($T type, left, right T) T { return right }
|
||||
`main proc() i16 { return same(i16(1), 2) }
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
infers_integer_builtin_results_from_their_type_or_rigid_operand test {
|
||||
try expect_specialization(
|
||||
`same proc($T type, left, right T) T { return right }
|
||||
`main proc() i16 {
|
||||
` upper :: same(maxval!(i16), i16(1))
|
||||
` lower :: same(minval!(i16), i16(2))
|
||||
` left :: same(divtrunc!(i16(8), 2), i16(3))
|
||||
` right :: same(divtrunc!(8, i16(2)), i16(4))
|
||||
` return upper + lower + left + right
|
||||
`}
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
infers_direct_results_before_contextual_literal_defaults test {
|
||||
try expect_specialization(
|
||||
`make proc($T type) T { return T(7) }
|
||||
`identity proc($T type, value T) T { return value }
|
||||
`sized proc($N usize, values [N]i16) [N]i16 { return values }
|
||||
`consume proc(value i16) i16 { return value }
|
||||
`main proc() i16 {
|
||||
` value i16 := identity(1)
|
||||
` value = make()
|
||||
` row [2]i16 :: sized([2, 3])
|
||||
` return consume(make()) + value + row[0]
|
||||
`}
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
infers_type_and_value_arguments_from_generated_nominal_results test {
|
||||
try expect_specialization(
|
||||
`Fixed proc($T type, $N usize) type {
|
||||
` return struct { value T = 0, count usize = N }
|
||||
`}
|
||||
`make proc($T type, $N usize) Fixed(T, N) { return {} }
|
||||
`identity proc($T type, $N usize, value T) Fixed(T, N) {
|
||||
` return { value = value }
|
||||
`}
|
||||
`main proc() i16 {
|
||||
` first Fixed(i16, 2) :: make()
|
||||
` second Fixed(i16, 3) :: identity(i16(7))
|
||||
` cached Fixed(i16, 2) :: make()
|
||||
` return first.value + second.value + cached.value
|
||||
`}
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
propagates_expected_results_through_nested_aggregates_and_success_channels test {
|
||||
try expect_specialization(
|
||||
`Error :: enum { failed }
|
||||
`Record :: struct { value i16, rows [2]mut i16 }
|
||||
`make proc($T type) T { return 7 }
|
||||
`optional proc() ?i16 { return make() }
|
||||
`fallible proc() i16 ! Error { return make() }
|
||||
`main proc() Record {
|
||||
` record Record := { value = make(), rows = [make(), make()] }
|
||||
` record.value = make()
|
||||
` record.rows[0] = make()
|
||||
` maybe ?i16 :: make()
|
||||
` result i16 ! Error :: make()
|
||||
` return { value = make(), rows = [make(), make()] }
|
||||
`}
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
propagates_expected_results_through_match_arms_and_value_block_yields test {
|
||||
try expect_specialization(
|
||||
`make proc($T type) T { return T(7) }
|
||||
`choose proc(flag bool) i16 {
|
||||
` return match flag {
|
||||
` true: make(),
|
||||
` false: { yield make() },
|
||||
` }
|
||||
`}
|
||||
`main proc() i16 {
|
||||
` value i16 :: { yield make() }
|
||||
` return value + choose(true)
|
||||
`}
|
||||
, .cannot_infer_generic, 0,
|
||||
)
|
||||
}
|
||||
|
||||
rejects_expected_results_that_conflict_with_rigid_runtime_arguments test {
|
||||
try expect_specialization(
|
||||
`identity proc($T type, value T) T { return value }
|
||||
`main proc() i16 { return identity(u8(7)) }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`Box proc($T type) type { return struct { value T } }
|
||||
`make proc($T type, value T) Box(T) { return { value = value } }
|
||||
`main proc() Box(i16) { return make(u8(7)) }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
}
|
||||
|
||||
rejects_unrelated_nominal_results_and_unresolved_compile_time_arguments test {
|
||||
try expect_specialization(
|
||||
`Box proc($T type) type { return struct { value T = 0 } }
|
||||
`Other proc($T type) type { return struct { value T = 0 } }
|
||||
`make proc($T type) Box(T) { return {} }
|
||||
`main proc() Other(i16) { return make() }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`make proc($T type, $N usize) T { return T(7) }
|
||||
`main proc() i16 { return make() }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`make proc($T type) T { return T(7) }
|
||||
`main proc() void { make() }
|
||||
, .cannot_infer_generic, 1,
|
||||
)
|
||||
}
|
||||
|
||||
preserves_explicit_arguments_and_explicit_type_procedure_inputs test {
|
||||
try expect_specialization(
|
||||
`identity proc($T type, value T) T { return value }
|
||||
`main proc() i16 { return identity(u8, 7) }
|
||||
, .return_type_mismatch, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`Box proc($T type) type { return struct { value T = 0 } }
|
||||
`make proc($T type) Box(T) { return {} }
|
||||
`main proc() Box(i16) { return make(u8) }
|
||||
, .return_type_mismatch, 1,
|
||||
)
|
||||
try expect_specialization(
|
||||
`Box proc($T type) type { return struct { value T } }
|
||||
`main proc() void { value Box() :: { value = 7 } }
|
||||
, .argument_count_mismatch, 1,
|
||||
)
|
||||
}
|
||||
@@ -4,13 +4,11 @@ import "@std/hashmap"
|
||||
|
||||
InternError :: enum { out_of_space }
|
||||
|
||||
#! identifies a string only within its owning pool.
|
||||
StringId :: alias u32
|
||||
|
||||
no_str :: maxval!(StringId)
|
||||
|
||||
#! global pool owned by main.hon; initialize before scanning and release with deinit.
|
||||
strings StringPool := undefined
|
||||
|
||||
#! retains its allocator and owns interned copies; release all storage with deinit.
|
||||
StringPool :: struct {
|
||||
ids hashmap.StringHashMap(StringId) # string → id
|
||||
|
||||
@@ -22,3 +22,19 @@ handles_interning test {
|
||||
try testing.expect_equal(null, invalid_str)
|
||||
try testing.expect_equal("hello", hello_str?)
|
||||
}
|
||||
|
||||
keeps_independent_pool_ids_local test {
|
||||
first := init(mem.c_allocator)
|
||||
defer deinit(&first)
|
||||
second := init(mem.c_allocator)
|
||||
defer deinit(&second)
|
||||
hello :: try intern(&first, "hello")
|
||||
world :: try intern(&second, "world")
|
||||
try testing.expect_equal(hello, world)
|
||||
try testing.expect_equal("hello", get_str(&first, hello)?)
|
||||
try testing.expect_equal("world", get_str(&second, world)?)
|
||||
different :: try intern(&first, "world")
|
||||
try testing.expect(different != hello)
|
||||
try testing.expect_equal(hello, try intern(&first, "hello"))
|
||||
try testing.expect_equal(world, try intern(&second, "world"))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ ArrayList proc($T type) type {
|
||||
#! retain `allocator` for later growth and cleanup; this call does not allocate.
|
||||
init proc($T type, allocator mem.Allocator) ArrayList(T) {
|
||||
return ArrayList(T) {
|
||||
items = mem.empty(T),
|
||||
items = &[],
|
||||
capacity = 0,
|
||||
allocator = allocator,
|
||||
}
|
||||
@@ -23,7 +23,7 @@ init proc($T type, allocator mem.Allocator) ArrayList(T) {
|
||||
deinit proc($T type, list @mut ArrayList(T)) void {
|
||||
allocation :: list.items.ptr[..list.capacity]
|
||||
mem.free(list.allocator, allocation)
|
||||
list.items = mem.empty(T)
|
||||
list.items = &[]
|
||||
list.capacity = 0
|
||||
}
|
||||
|
||||
@@ -38,15 +38,10 @@ reserve proc(
|
||||
new_capacity := 8
|
||||
if list.capacity >= 8 {
|
||||
half :: divtrunc!(list.capacity, 2)
|
||||
if list.capacity > maxval!(usize) - half {
|
||||
new_capacity = min_capacity
|
||||
} else {
|
||||
new_capacity = list.capacity + half
|
||||
}
|
||||
}
|
||||
if new_capacity < min_capacity {
|
||||
new_capacity = min_capacity
|
||||
if (list.capacity > maxval!(usize) - half) new_capacity = min_capacity
|
||||
else new_capacity = list.capacity + half
|
||||
}
|
||||
if (new_capacity < min_capacity) new_capacity = min_capacity
|
||||
|
||||
length :: list.items.len
|
||||
allocation :: list.items.ptr[..list.capacity]
|
||||
@@ -60,9 +55,7 @@ reserve proc(
|
||||
|
||||
append proc($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
|
||||
length :: list.items.len
|
||||
if length == maxval!(usize) {
|
||||
return .out_of_memory
|
||||
}
|
||||
if (length == maxval!(usize)) return .out_of_memory
|
||||
try reserve(list, length + 1)
|
||||
list.items = list.items.ptr[..length + 1]
|
||||
list.items[length] = value
|
||||
|
||||
+22
-2
@@ -15,6 +15,25 @@ EnumMap proc($E, $V type) type {
|
||||
init proc(
|
||||
$E, $V type,
|
||||
values meta.EnumFieldStruct(E, ?V, some!(null)),
|
||||
) EnumMap(E, V) {
|
||||
return initialize(E, V, null, values)
|
||||
}
|
||||
|
||||
#! initializes absent value slots without making their keys present.
|
||||
#! use fully initialized storage when a sparse map must be materialized at compile time.
|
||||
init_default proc(
|
||||
$E, $V type,
|
||||
storage_default V,
|
||||
values meta.EnumFieldStruct(E, ?V, some!(null)),
|
||||
) EnumMap(E, V) {
|
||||
return initialize(E, V, some!(storage_default), values)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
initialize proc(
|
||||
$E, $V type,
|
||||
storage_default ?V,
|
||||
values meta.EnumFieldStruct(E, ?V, some!(null)),
|
||||
) EnumMap(E, V) {
|
||||
map EnumMap(E, V) := undefined
|
||||
|
||||
@@ -25,7 +44,7 @@ init proc(
|
||||
if field!(values, field.name) |value| {
|
||||
map.present[i] = true
|
||||
map.values[i] = value
|
||||
}
|
||||
} else if (storage_default) |value| map.values[i] = value
|
||||
}
|
||||
else: compile_error!("EnumMap key must be an enum")
|
||||
}
|
||||
@@ -38,10 +57,11 @@ get proc($E, $V type, map @EnumMap(E, V), key E) ?V {
|
||||
match typeinfo!(E) {
|
||||
.enum |info|: inline for info.fields |field, i| {
|
||||
if key == field!(E, field.name) {
|
||||
if (map.present[i]) return map.values[i]
|
||||
if (map.present[i]) return some!(map.values[i])
|
||||
return null
|
||||
}
|
||||
}
|
||||
else: compile_error!("EnumMap key must be an enum")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -25,6 +25,22 @@ handles_sparse_enum_get test {
|
||||
try testing.expect_equal(null, eof)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
sparse_optional_values :: $init_default(TestEnum, ?i16, null, {
|
||||
ident = some!(null),
|
||||
int = some!(some!(i16(42))),
|
||||
})
|
||||
|
||||
filled_comptime_storage_preserves_absence_and_nested_optional_presence test {
|
||||
absent :: get(&sparse_optional_values, TestEnum.eof)
|
||||
present_null :: get(&sparse_optional_values, TestEnum.ident)
|
||||
present_value :: get(&sparse_optional_values, TestEnum.int)
|
||||
try testing.expect(absent == null)
|
||||
try testing.expect(present_null != null)
|
||||
try testing.expect(present_null? == null)
|
||||
try testing.expect_equal(i16(42), present_value??)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
TestLeftError :: enum {
|
||||
shared
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import "@ffi/c"
|
||||
import "@std/mem"
|
||||
import "@std/io"
|
||||
import "@std/arraylist"
|
||||
|
||||
PathError :: enum { invalid_path, path_failed, directory_failed }
|
||||
|
||||
Error :: alias mem.AllocError | PathError
|
||||
ReadFileError :: alias Error | io.OpenError | io.CloseError | io.ReadError | io.ReadLimitError
|
||||
|
||||
Kind :: enum { missing, file, directory, link, unknown, other }
|
||||
|
||||
#! owns a native directory handle; release it exactly once with close_directory.
|
||||
Directory :: struct { handle ?*mut c.DIR }
|
||||
|
||||
#! name borrows directory-owned storage until the next call to next or close_directory.
|
||||
#! enumeration may report link or unknown; kind follows links when classification is needed.
|
||||
Entry :: struct {
|
||||
name []u8
|
||||
kind Kind
|
||||
}
|
||||
|
||||
#! returns caller-owned zero-terminated storage; release it with mem.free using the same allocator.
|
||||
@hide:file
|
||||
terminated proc(allocator mem.Allocator, text []u8) []mut u8 ! Error {
|
||||
if (text.len == 0 or text.len == maxval!(usize)) return .invalid_path
|
||||
for (text) |byte| {
|
||||
if (byte == 0) return .invalid_path
|
||||
}
|
||||
bytes :: try mem.alloc(u8, allocator, text.len + 1)
|
||||
memcopy!(bytes[..text.len], text)
|
||||
bytes[text.len] = 0
|
||||
return bytes
|
||||
}
|
||||
|
||||
#! returns an owned absolute path with symlinks resolved; release it with mem.free using the same allocator.
|
||||
#! temporary path storage uses `allocator` and is released before returning.
|
||||
canonical proc(allocator mem.Allocator, path []u8) []mut u8 ! Error {
|
||||
input :: try terminated(allocator, path)
|
||||
defer mem.free(allocator, input)
|
||||
# Darwin realpath requires a PATH_MAX-sized caller buffer, including its terminator.
|
||||
buffer [1024]mut u8 := undefined
|
||||
if (c.realpath(ptrcast!(input.ptr), ptrcast!(buffer[..].ptr)) == null) {
|
||||
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
|
||||
return .path_failed
|
||||
}
|
||||
length := usize(0)
|
||||
while (length < buffer.len and buffer[length] != 0) length += 1
|
||||
if (length == buffer.len) return .path_failed
|
||||
owned :: try mem.alloc(u8, allocator, length)
|
||||
memcopy!(owned, buffer[..length])
|
||||
return owned
|
||||
}
|
||||
|
||||
#! follows symlinks; nonexistent paths (including dangling links) return missing.
|
||||
#! temporary path storage uses `allocator` and is released before returning.
|
||||
kind proc(allocator mem.Allocator, path []u8) Kind ! Error {
|
||||
input :: try terminated(allocator, path)
|
||||
defer mem.free(allocator, input)
|
||||
result [1]mut c.Stat := undefined
|
||||
if (c.stat(ptrcast!(input.ptr), result[..].ptr) == 0) {
|
||||
mode :: result[0].st_mode & c_ushort(61440)
|
||||
if (mode == 16384) return .directory
|
||||
if (mode == 32768) return .file
|
||||
return .other
|
||||
}
|
||||
error :: c.__error()?^
|
||||
if (error == c.ENOMEM) return .out_of_memory
|
||||
if (error == c.ENOENT or error == c.ENOTDIR) return .missing
|
||||
return .path_failed
|
||||
}
|
||||
|
||||
exists proc(allocator mem.Allocator, path []u8) bool ! Error {
|
||||
return (try kind(allocator, path)) != .missing
|
||||
}
|
||||
|
||||
#! opens an owned directory handle; `path` is borrowed only during this call.
|
||||
#! temporary path storage uses `allocator` and is released before returning.
|
||||
#! release the returned handle with close_directory.
|
||||
open_directory proc(allocator mem.Allocator, path []u8) Directory ! Error {
|
||||
input :: try terminated(allocator, path)
|
||||
defer mem.free(allocator, input)
|
||||
handle :: c.opendir(ptrcast!(input.ptr))
|
||||
if (handle == null) {
|
||||
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
|
||||
return .directory_failed
|
||||
}
|
||||
return Directory{ handle = handle }
|
||||
}
|
||||
|
||||
#! returns the next borrowed entry, omitting . and ..; null marks the end.
|
||||
#! order is filesystem-defined. no allocation is performed; each name expires on the next call.
|
||||
next proc(directory @Directory) ?Entry ! Error {
|
||||
if (directory.handle == null) return .directory_failed
|
||||
while true {
|
||||
c.__error()?^ = 0
|
||||
item :: c.readdir(directory.handle)
|
||||
if (item == null) {
|
||||
if (c.__error()?^ == c.ENOMEM) return .out_of_memory
|
||||
if (c.__error()?^ != 0) return .directory_failed
|
||||
end ?Entry :: null
|
||||
return end
|
||||
}
|
||||
entry :: item?
|
||||
length :: usize(entry.d_namlen)
|
||||
if (length >= 1024) return .directory_failed
|
||||
pointer *u8 :: ptrcast!(entry.d_name[..].ptr)
|
||||
name :: pointer[..length]
|
||||
if (mem.eql(name, ".") or mem.eql(name, "..")) continue
|
||||
entry_kind Kind :: match entry.d_type {
|
||||
0: .unknown
|
||||
4: .directory
|
||||
8: .file
|
||||
10: .link
|
||||
else: .other
|
||||
}
|
||||
return Entry{ name = name, kind = entry_kind }
|
||||
}
|
||||
}
|
||||
|
||||
#! invalidates the handle, including when native close reports failure.
|
||||
close_directory proc(directory @mut Directory) void ! Error {
|
||||
handle :: directory.handle
|
||||
if (handle == null) return .directory_failed
|
||||
directory.handle = null
|
||||
if (c.closedir(handle) != 0) return .directory_failed
|
||||
}
|
||||
|
||||
#! reads a regular file through runtime IO, accepting at most `maximum` bytes.
|
||||
#! follows symlinks and rejects devices/FIFOs before opening; metadata uses the host filesystem.
|
||||
#! returned storage belongs to the caller and retains `allocator`; release with arraylist.deinit. all errors close the file.
|
||||
#! temporary path storage uses `allocator` and is released before returning.
|
||||
read_file proc(
|
||||
allocator mem.Allocator,
|
||||
runtime io.Io,
|
||||
path []u8,
|
||||
maximum usize,
|
||||
) arraylist.ArrayList(u8) ! ReadFileError {
|
||||
if (try kind(allocator, path) != .file) return .open_failed
|
||||
input :: try terminated(allocator, path)
|
||||
defer mem.free(allocator, input)
|
||||
file :: try io.open(runtime, input.ptr[..path.len;0], .read_only)
|
||||
closing := false
|
||||
defer { if (!closing) io.close(file) catch |_| {} }
|
||||
bytes := try io.read_all(allocator, io.reader(file), maximum)
|
||||
errdefer arraylist.deinit(&bytes)
|
||||
closing = true
|
||||
try io.close(file)
|
||||
return bytes
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "@std/mem"
|
||||
import "@std/testing"
|
||||
|
||||
rejects_empty_paths_and_embedded_nul_instead_of_truncating test {
|
||||
paths [2][]u8 :: ["", ".\0not-part-of-the-path"]
|
||||
for (paths) |path| {
|
||||
resolved :: canonical(mem.c_allocator, path) catch |err| {
|
||||
try testing.expect(err == .invalid_path)
|
||||
continue
|
||||
}
|
||||
mem.free(mem.c_allocator, resolved)
|
||||
return .expectation_failed
|
||||
}
|
||||
}
|
||||
|
||||
closed_directory_cannot_be_read_or_closed_again test {
|
||||
directory := try open_directory(mem.c_allocator, ".")
|
||||
try close_directory(&directory)
|
||||
_ = next(&directory) catch |err| {
|
||||
try testing.expect(err == .directory_failed)
|
||||
close_directory(&directory) catch |close_error| {
|
||||
try testing.expect(close_error == .directory_failed)
|
||||
return
|
||||
}
|
||||
return .expectation_failed
|
||||
}
|
||||
return .expectation_failed
|
||||
}
|
||||
+111
-23
@@ -1,4 +1,5 @@
|
||||
import "@std/mem"
|
||||
import "@std/meta"
|
||||
|
||||
PutError :: enum { key_exists }
|
||||
|
||||
@@ -13,10 +14,11 @@ Entry proc($K, $V type) type {
|
||||
|
||||
#! owns entry storage and retains its allocator; release storage with deinit.
|
||||
#! keys and values are stored by value; any resources they reference remain caller-owned.
|
||||
#! null callbacks select automatic operations; equal keys must always produce equal hashes.
|
||||
HashMap proc(
|
||||
$K, $V type,
|
||||
$hash_key proc(key K) usize,
|
||||
$keys_eql proc(a, b K) bool,
|
||||
$hash_key ?proc(key K) usize,
|
||||
$keys_eql ?proc(a, b K) bool,
|
||||
) type {
|
||||
return struct {
|
||||
entries []mut Entry(K, V)
|
||||
@@ -29,15 +31,22 @@ StringHashMap proc($V type) type {
|
||||
return HashMap([]u8, V, str_hash, str_eql)
|
||||
}
|
||||
|
||||
#! automatic value hashing for integers, booleans, enums, distinct types, arrays and records.
|
||||
#! slices and floating-point keys require explicit hash/equality operations through HashMap.
|
||||
#! record padding is ignored; pointer, union and optional keys are not supported.
|
||||
AutoHashMap proc($K, $V type) type {
|
||||
return HashMap(K, V, null, null)
|
||||
}
|
||||
|
||||
#! retain `allocator` for later growth and cleanup; this call does not allocate.
|
||||
init proc(
|
||||
$K, $V type,
|
||||
$hash_key proc(key K) usize,
|
||||
$keys_eql proc(a, b K) bool,
|
||||
$hash_key ?proc(key K) usize,
|
||||
$keys_eql ?proc(a, b K) bool,
|
||||
allocator mem.Allocator,
|
||||
) HashMap(K, V, hash_key, keys_eql) {
|
||||
return HashMap(K, V, hash_key, keys_eql){
|
||||
entries = mem.empty(Entry(K, V)),
|
||||
entries = &[],
|
||||
count = 0,
|
||||
allocator = allocator,
|
||||
}
|
||||
@@ -47,29 +56,35 @@ init proc(
|
||||
#! note: this operation invalidates the map.
|
||||
deinit proc(
|
||||
$K, $V type,
|
||||
$hash_key proc(key K) usize,
|
||||
$keys_eql proc(a, b K) bool,
|
||||
$hash_key ?proc(key K) usize,
|
||||
$keys_eql ?proc(a, b K) bool,
|
||||
map @HashMap(K, V, hash_key, keys_eql),
|
||||
) void { mem.free(map.allocator, map.entries) }
|
||||
|
||||
get proc(
|
||||
$K, $V type,
|
||||
$hash_key proc(key K) usize,
|
||||
$keys_eql proc(a, b K) bool,
|
||||
$hash_key ?proc(key K) usize,
|
||||
$keys_eql ?proc(a, b K) bool,
|
||||
map @HashMap(K, V, hash_key, keys_eql),
|
||||
key K,
|
||||
) ?V {
|
||||
if (map.count == 0) return null
|
||||
|
||||
hash :: normalize(hash_key(key))
|
||||
hash usize := 0
|
||||
if ($(hash_key == null)) hash = normalize(auto_hash(K, key))
|
||||
else hash = normalize(call_hash(K, hash_key?, key))
|
||||
idx := hash & (map.entries.len - 1)
|
||||
|
||||
while true {
|
||||
entry :: map.entries[idx]
|
||||
if (entry.hash == 0) return null
|
||||
|
||||
if (entry.hash == hash and keys_eql(entry.key, key)) {
|
||||
return entry.value
|
||||
if (entry.hash == hash) {
|
||||
if $(keys_eql == null) {
|
||||
if (auto_eql(K, entry.key, key)) return entry.value
|
||||
} else {
|
||||
if (call_eql(K, keys_eql?, entry.key, key)) return entry.value
|
||||
}
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (map.entries.len - 1)
|
||||
@@ -78,8 +93,8 @@ get proc(
|
||||
|
||||
put proc(
|
||||
$K, $V type,
|
||||
$hash_key proc(key K) usize,
|
||||
$keys_eql proc(a, b K) bool,
|
||||
$hash_key ?proc(key K) usize,
|
||||
$keys_eql ?proc(a, b K) bool,
|
||||
map @mut HashMap(K, V, hash_key, keys_eql),
|
||||
key K,
|
||||
value V,
|
||||
@@ -95,19 +110,19 @@ put proc(
|
||||
for old_entries |entry| {
|
||||
if (entry.hash == 0) continue
|
||||
|
||||
idx := entry.hash & (new_entries.len - 1)
|
||||
while new_entries[idx].hash != 0 {
|
||||
idx = (idx + 1) & (new_entries.len - 1)
|
||||
}
|
||||
rehash_index := entry.hash & (new_entries.len - 1)
|
||||
while (new_entries[rehash_index].hash != 0) rehash_index = (rehash_index + 1) & (new_entries.len - 1)
|
||||
|
||||
new_entries[idx] = entry
|
||||
new_entries[rehash_index] = entry
|
||||
}
|
||||
|
||||
map.entries = new_entries
|
||||
mem.free(map.allocator, old_entries)
|
||||
}
|
||||
|
||||
hash :: normalize(hash_key(key))
|
||||
hash usize := 0
|
||||
if ($(hash_key == null)) hash = normalize(auto_hash(K, key))
|
||||
else hash = normalize(call_hash(K, hash_key?, key))
|
||||
idx := hash & (map.entries.len - 1)
|
||||
|
||||
while true {
|
||||
@@ -122,14 +137,29 @@ put proc(
|
||||
return
|
||||
}
|
||||
|
||||
if entry.hash == hash and keys_eql(entry.key, key) {
|
||||
return .key_exists
|
||||
if (entry.hash == hash) {
|
||||
if $(keys_eql == null) {
|
||||
if (auto_eql(K, entry.key, key)) return .key_exists
|
||||
} else {
|
||||
if (call_eql(K, keys_eql?, entry.key, key)) return .key_exists
|
||||
}
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (map.entries.len - 1)
|
||||
}
|
||||
}
|
||||
|
||||
# concrete comptime callbacks use named calls so their specializations are registered.
|
||||
@hide:file
|
||||
call_hash proc($K type, $callback proc(key K) usize, key K) usize {
|
||||
return callback(key)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
call_eql proc($K type, $callback proc(a, b K) bool, a, b K) bool {
|
||||
return callback(a, b)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
normalize proc(hash usize) usize {
|
||||
# mapping both 0 and 1 to 1 is safe because equality resolves
|
||||
@@ -146,7 +176,7 @@ str_hash proc(key []u8) usize {
|
||||
prime u32 :: 16777619
|
||||
|
||||
for key |byte| {
|
||||
product u64 :: u64(hash xor u32(byte)) * prime
|
||||
product u64 :: u64(hash xor u32(byte)) * u64(prime)
|
||||
hash = u32(product & u64(maxval!(u32)))
|
||||
}
|
||||
|
||||
@@ -155,3 +185,61 @@ str_hash proc(key []u8) usize {
|
||||
|
||||
@hide:file
|
||||
str_eql proc(a, b []u8) bool { return mem.eql(a, b) }
|
||||
|
||||
@hide:file
|
||||
auto_hash proc($K type, key K) usize {
|
||||
match typeinfo!(K) {
|
||||
.integer: {
|
||||
bytes @[sizeof!(K)]u8 :: ptrcast!(&key)
|
||||
return str_hash(bytes[..])
|
||||
}
|
||||
.bool: return if (key) 1 else 0
|
||||
.enum |info|: {
|
||||
inline for (info.fields) |field| {
|
||||
if (key == field!(K, field.name)) return field.index
|
||||
}
|
||||
unreachable
|
||||
}
|
||||
.distinct |backing|: return auto_hash(ptrcast!(@backing, &key)^)
|
||||
.array: {
|
||||
hash usize := 0
|
||||
for (key) |element| hash = combine_hash(hash, auto_hash(element))
|
||||
return hash
|
||||
}
|
||||
.record |info|: {
|
||||
hash usize := 0
|
||||
inline for (info.fields) |field| {
|
||||
hash = combine_hash(hash, auto_hash(field!(key, field.name)))
|
||||
}
|
||||
return hash
|
||||
}
|
||||
else: compile_error!("AutoHashMap key requires integers, booleans, enums, distinct types, arrays or records; use HashMap for custom hashing")
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
combine_hash proc(left, right usize) usize {
|
||||
# rotate before mixing so ordered fields do not cancel like a plain xor.
|
||||
return ((left << 5) xor (left >> (sizeof!(usize) * 8 - 5))) xor right
|
||||
}
|
||||
|
||||
@hide:file
|
||||
auto_eql proc($K type, left, right K) bool {
|
||||
match typeinfo!(K) {
|
||||
.integer, .bool, .enum: return left == right
|
||||
.distinct |backing|: return auto_eql(ptrcast!(@backing, &left)^, ptrcast!(@backing, &right)^)
|
||||
.array: {
|
||||
for (left) |element, index| {
|
||||
if (!auto_eql(element, right[index])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
.record |info|: {
|
||||
inline for (info.fields) |field| {
|
||||
if (!auto_eql(field!(left, field.name), field!(right, field.name))) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
else: compile_error!("AutoHashMap key requires integers, booleans, enums, distinct types, arrays or records; use HashMap for custom equality")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,78 @@ handles_put_and_get test {
|
||||
try testing.expect_type(?u32, value)
|
||||
try testing.expect_equal(42, value?)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
collision_hash proc(_ u32) usize { return 0 }
|
||||
|
||||
@hide:file
|
||||
collision_eql proc(left, right u32) bool {
|
||||
return rem!(left, 100) == rem!(right, 100)
|
||||
}
|
||||
|
||||
custom_callbacks_preserve_collisions_and_equivalent_keys test {
|
||||
map HashMap(u32, u32, collision_hash, collision_eql) := init(mem.c_allocator)
|
||||
defer deinit(&map)
|
||||
for (u32(1)..u32(32)) |key| try put(&map, key, key * 2)
|
||||
for (u32(1)..u32(32)) |key| {
|
||||
try testing.expect_equal(key * 2, get(&map, key + 100)?)
|
||||
}
|
||||
try testing.expect_equal(null, get(&map, 99))
|
||||
duplicate := false
|
||||
put(&map, 101, 999) catch |error| { duplicate = error == .key_exists }
|
||||
try testing.expect(duplicate)
|
||||
try testing.expect_equal(u32(31), u32(map.count))
|
||||
try testing.expect_equal(u32(2), get(&map, 1)?)
|
||||
}
|
||||
|
||||
auto_integer_keys_preserve_boundaries_and_rehashing test {
|
||||
map AutoHashMap(i64, usize) := init(mem.c_allocator)
|
||||
defer deinit(&map)
|
||||
keys [_]i64 :: [minval!(i64), -1, 0, 1, maxval!(i64)]
|
||||
for (keys) |key, index| try put(&map, key, index)
|
||||
for (i64(2)..i64(100)) |key| try put(&map, key, usize(key))
|
||||
for (keys) |key, index| try testing.expect_equal(index, get(&map, key)?)
|
||||
for (i64(2)..i64(100)) |key| try testing.expect_equal(usize(key), get(&map, key)?)
|
||||
try testing.expect_equal(null, get(&map, -2))
|
||||
duplicate := false
|
||||
put(&map, -1, 999) catch |error| { duplicate = error == .key_exists }
|
||||
try testing.expect(duplicate)
|
||||
try testing.expect_equal(usize(1), get(&map, -1)?)
|
||||
wide AutoHashMap(u64, bool) := init(mem.c_allocator)
|
||||
defer deinit(&wide)
|
||||
try put(&wide, maxval!(u64), true)
|
||||
try testing.expect_equal(true, get(&wide, maxval!(u64))?)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
TestKeyKind :: enum(u8) { first = 3, second = 91 }
|
||||
|
||||
@hide:file
|
||||
TestKeyId :: distinct u32
|
||||
|
||||
@hide:file
|
||||
TestKey :: struct {
|
||||
enabled bool
|
||||
id TestKeyId
|
||||
kind TestKeyKind
|
||||
indices [2]u16
|
||||
}
|
||||
|
||||
auto_record_keys_use_field_values_and_array_order test {
|
||||
map AutoHashMap(TestKey, i32) := init(mem.c_allocator)
|
||||
defer deinit(&map)
|
||||
first :: TestKey{ enabled = true, id = TestKeyId(17), kind = .first, indices = [2, 9] }
|
||||
reversed :: TestKey{ enabled = true, id = TestKeyId(17), kind = .first, indices = [9, 2] }
|
||||
second :: TestKey{ enabled = false, id = TestKeyId(18), kind = .second, indices = [2, 9] }
|
||||
try put(&map, first, 42)
|
||||
try put(&map, reversed, 51)
|
||||
try put(&map, second, 63)
|
||||
try testing.expect_equal(i32(42), get(&map, TestKey{
|
||||
enabled = true, id = TestKeyId(17), kind = .first, indices = [2, 9],
|
||||
})?)
|
||||
try testing.expect_equal(i32(51), get(&map, reversed)?)
|
||||
try testing.expect_equal(i32(63), get(&map, second)?)
|
||||
try testing.expect_equal(null, get(&map, TestKey{
|
||||
enabled = true, id = TestKeyId(18), kind = .first, indices = [2, 9],
|
||||
}))
|
||||
}
|
||||
|
||||
+6
-18
@@ -269,9 +269,7 @@ parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken {
|
||||
while cursor < format.len {
|
||||
byte :: format[cursor]
|
||||
if byte == '{' {
|
||||
if cursor + 1 >= format.len {
|
||||
compile_error!("io.print format has an unmatched '{'")
|
||||
}
|
||||
if (cursor + 1 >= format.len) compile_error!("io.print format has an unmatched '{'")
|
||||
if cursor > literal_start {
|
||||
tokens[token_count] = FormatToken{
|
||||
kind = .literal,
|
||||
@@ -298,9 +296,7 @@ parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken {
|
||||
kind FormatTokenKind := .default
|
||||
width := 2
|
||||
if next != '}' {
|
||||
if cursor + 2 >= format.len or format[cursor + 2] != '}' {
|
||||
compile_error!("io.print format expects a one-character specifier")
|
||||
}
|
||||
if (cursor + 2 >= format.len or format[cursor + 2] != '}') compile_error!("io.print format expects a one-character specifier")
|
||||
|
||||
width = 3
|
||||
if (next == 's') kind = .string
|
||||
@@ -314,9 +310,7 @@ parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken {
|
||||
else compile_error!("io.print format has an unknown specifier")
|
||||
}
|
||||
|
||||
if argument_count >= field_count {
|
||||
compile_error!("io.print format argument count does not match the tuple")
|
||||
}
|
||||
if (argument_count >= field_count) compile_error!("io.print format argument count does not match the tuple")
|
||||
|
||||
tokens[token_count] = FormatToken{
|
||||
kind = kind,
|
||||
@@ -333,9 +327,7 @@ parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken {
|
||||
}
|
||||
|
||||
if byte == '}' {
|
||||
if cursor + 1 >= format.len or format[cursor + 1] != '}' {
|
||||
compile_error!("io.print format has an unmatched '}'")
|
||||
}
|
||||
if (cursor + 1 >= format.len or format[cursor + 1] != '}') compile_error!("io.print format has an unmatched '}'")
|
||||
if cursor > literal_start {
|
||||
tokens[token_count] = FormatToken{
|
||||
kind = .literal,
|
||||
@@ -368,9 +360,7 @@ parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken {
|
||||
}
|
||||
}
|
||||
|
||||
if argument_count != field_count {
|
||||
compile_error!("io.print format argument count does not match the tuple")
|
||||
}
|
||||
if (argument_count != field_count) compile_error!("io.print format argument count does not match the tuple")
|
||||
return tokens
|
||||
}
|
||||
|
||||
@@ -466,9 +456,7 @@ write_decimal proc($T type, output Writer, value T) void ! WriteError {
|
||||
write_character proc($T type, output Writer, value T) void ! WriteError {
|
||||
match typeinfo!(T) {
|
||||
.integer: {
|
||||
if minval!(T) < 0 or maxval!(T) > 255 {
|
||||
compile_error!("io.print '{c}' requires an unsigned integer that fits in u8")
|
||||
}
|
||||
if (minval!(T) < 0 or maxval!(T) > 255) compile_error!("io.print '{c}' requires an unsigned integer that fits in u8")
|
||||
buffer [1]u8 :: [u8(value)]
|
||||
try write_all(output, buffer[..])
|
||||
}
|
||||
|
||||
+11
-19
@@ -26,7 +26,8 @@ raw_alloc proc(
|
||||
}
|
||||
|
||||
#! resize caller-owned storage with its original allocator and alignment.
|
||||
#! on allocation failure with nonzero `new_size`, original storage remains caller-owned; release the result with raw_free.
|
||||
#! on allocation failure with nonzero `new_size`, original storage remains caller-owned.
|
||||
#! release returned storage with raw_free using the same allocator and alignment, with `new_size` as its size.
|
||||
raw_realloc proc(
|
||||
allocator Allocator,
|
||||
memory ?*mut u8,
|
||||
@@ -49,7 +50,9 @@ raw_free proc(
|
||||
|
||||
eql proc($T type, left, right []T) bool {
|
||||
if (left.len != right.len) return false
|
||||
for (0..left.len) |i| if (left[i] != right[i]) return false
|
||||
for (0..left.len) |i| {
|
||||
if (left[i] != right[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -62,14 +65,12 @@ set proc($T type, destination []mut T, value T) void {
|
||||
#! allocate memory for a slice of type `T` with `count` elements.
|
||||
#! returned storage belongs to the caller; release it with free using the same allocator.
|
||||
alloc proc($T type, allocator Allocator, count usize) []mut T ! AllocError {
|
||||
if (count == 0) return empty_slice(T, 0)
|
||||
if (count == 0) return &[]
|
||||
|
||||
element_size :: sizeof!(T)
|
||||
if (element_size == 0) return empty_slice(T, count)
|
||||
|
||||
if count > divtrunc!(maxval!(usize), element_size) {
|
||||
return .out_of_memory
|
||||
}
|
||||
if (count > divtrunc!(maxval!(usize), element_size)) return .out_of_memory
|
||||
|
||||
memory :: raw_alloc(allocator, count * element_size, alignof!(T))
|
||||
if memory |bytes| {
|
||||
@@ -95,7 +96,7 @@ realloc proc(
|
||||
|
||||
if new_count == 0 {
|
||||
free(allocator, memory)
|
||||
return empty_slice(T, 0)
|
||||
return &[]
|
||||
}
|
||||
|
||||
element_size :: sizeof!(T)
|
||||
@@ -137,21 +138,14 @@ free proc($T type, allocator Allocator, memory []T) void {
|
||||
)
|
||||
}
|
||||
|
||||
#! create a slice backed by `empty_storage` without allocating.
|
||||
#! precondition: `count` must be 0 or `sizeof!(T)` must be 0; otherwise the slice
|
||||
#! exceeds its backing storage.
|
||||
#! create a logically nonempty slice of zero-sized elements without allocating.
|
||||
#! precondition: sizeof!(T) must be 0; no element storage is accessed.
|
||||
@hide:file
|
||||
empty_slice proc($T type, count usize) []mut T {
|
||||
pointer *mut T :: ptrcast!((&empty_storage).ptr)
|
||||
return pointer[..count]
|
||||
}
|
||||
|
||||
#! get an empty slice of type `T` with 0 elements.
|
||||
#! the view borrows shared empty storage and requires no cleanup.
|
||||
empty proc($T type) []mut T {
|
||||
return empty_slice(T, 0)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
empty_storage [1]mut u64 := [0]
|
||||
|
||||
@@ -198,9 +192,7 @@ c_realloc proc(
|
||||
}
|
||||
|
||||
if memory |old_memory| {
|
||||
if alignment <= malloc_alignment {
|
||||
return ptrcast!(c.realloc(old_memory, c_ulong(new_size)))
|
||||
}
|
||||
if (alignment <= malloc_alignment) return ptrcast!(c.realloc(old_memory, c_ulong(new_size)))
|
||||
|
||||
new_memory :: c_alloc(null, new_size, alignment)
|
||||
if new_memory |new_bytes| {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import "@std/io"
|
||||
import "@ffi/c"
|
||||
import "@std/mem"
|
||||
|
||||
Init :: struct {
|
||||
io io.Io
|
||||
@@ -9,3 +11,58 @@ Init :: struct {
|
||||
Args :: struct {
|
||||
values [][;0]u8
|
||||
}
|
||||
|
||||
RunError :: enum {
|
||||
invalid_arguments
|
||||
spawn_failed
|
||||
wait_failed
|
||||
}
|
||||
|
||||
#! run a program synchronously without a shell, searching PATH for `arguments[0]`.
|
||||
#! `arguments` and zero-terminated strings are borrowed until this call returns.
|
||||
#! an empty argument vector, empty executable name, or embedded NUL is invalid.
|
||||
#! the child inherits the current environment and stdin/stdout/stderr.
|
||||
#! temporary argument descriptors use `allocator` and are freed on every return.
|
||||
#! return the exit code, or 128 plus the terminating signal number.
|
||||
run proc(allocator mem.Allocator, arguments [][;0]u8) i32 ! (mem.AllocError | RunError) {
|
||||
if (arguments.len == 0) return .invalid_arguments
|
||||
if (arguments[0].len == 0) return .invalid_arguments
|
||||
for arguments |argument| {
|
||||
for (argument) |byte| {
|
||||
if (byte == 0) return .invalid_arguments
|
||||
}
|
||||
}
|
||||
if (arguments.len == maxval!(usize)) return .out_of_memory
|
||||
|
||||
descriptors :: try mem.alloc(?*mut c_char, allocator, arguments.len + 1)
|
||||
defer mem.free(allocator, descriptors)
|
||||
for (0..arguments.len) |index| {
|
||||
descriptors[index] = ptrcast!(constcast!(arguments[index]).ptr)
|
||||
}
|
||||
descriptors[arguments.len] = null
|
||||
|
||||
pid [1]mut c_int := [0]
|
||||
result :: c.posix_spawnp(
|
||||
pid[..].ptr,
|
||||
ptrcast!(arguments[0].ptr),
|
||||
null,
|
||||
null,
|
||||
descriptors.ptr,
|
||||
c._NSGetEnviron()?^,
|
||||
)
|
||||
if (result != 0) return .spawn_failed
|
||||
|
||||
status [1]mut c_int := [0]
|
||||
while true {
|
||||
waited :: c.waitpid(pid[0], status[..].ptr, 0)
|
||||
if (waited < 0) {
|
||||
if (c.__error()?^ == c.EINTR) continue
|
||||
return .wait_failed
|
||||
}
|
||||
# Darwin wait status: low seven bits hold the signal, next byte the exit code.
|
||||
signal :: status[0] & 127
|
||||
if (signal == 0) return i32(divtrunc!(status[0], 256) & 255)
|
||||
if (signal != 127) return i32(128 + signal)
|
||||
# a traced child can report a stop even without WUNTRACED; wait for termination.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import "@std/testing"
|
||||
import "@std/mem"
|
||||
|
||||
runs_without_shell_and_preserves_exit_status test {
|
||||
arguments [4][;0]u8 :: ["/bin/test", "a b;$(exit 99)", "=", "a b;$(exit 99)"]
|
||||
status :: run(mem.c_allocator, arguments[..]) catch |_| { return .expectation_failed }
|
||||
try testing.expect_equal(i32(0), status)
|
||||
|
||||
failing [2][;0]u8 :: ["/bin/test", ""]
|
||||
failure :: run(mem.c_allocator, failing[..]) catch |_| { return .expectation_failed }
|
||||
try testing.expect_equal(i32(1), failure)
|
||||
}
|
||||
|
||||
rejects_invalid_arguments_and_reports_spawn_failure test {
|
||||
empty [0][;0]u8 :: []
|
||||
_ = run(mem.c_allocator, empty[..]) catch |err| {
|
||||
try testing.expect(err == .invalid_arguments)
|
||||
missing [1][;0]u8 :: ["/nonexistent/honey-process-test"]
|
||||
_ = run(mem.c_allocator, missing[..]) catch |spawn_error| {
|
||||
try testing.expect(spawn_error == .spawn_failed)
|
||||
return
|
||||
}
|
||||
return .expectation_failed
|
||||
}
|
||||
return .expectation_failed
|
||||
}
|
||||
@@ -18,21 +18,17 @@ Pair proc($V type) type {
|
||||
#! initializes a static string map from a list of key-value pairs (constructed at compile-time).
|
||||
#! returned views use compile-time storage and require no cleanup.
|
||||
init proc($V type, $N usize, $entries [N]Pair(V)) StaticStringMap(V) {
|
||||
if N > usize(maxval!(u32)) {
|
||||
compile_error!("static string map has too many entries")
|
||||
}
|
||||
if (N > usize(maxval!(u32))) compile_error!("static string map has too many entries")
|
||||
|
||||
keys [N]mut []u8 := undefined
|
||||
values [N]mut V := undefined
|
||||
|
||||
# assert no duplicate keys
|
||||
for entries |entry, i| {
|
||||
if entry.0.len > usize(maxval!(u32)) {
|
||||
compile_error!("static string map key is too long")
|
||||
}
|
||||
if (entry.0.len > usize(maxval!(u32))) compile_error!("static string map key is too long")
|
||||
|
||||
for (0..i) |prior| if mem.eql(u8, entry.0, entries[prior].0) {
|
||||
compile_error!("duplicate static string map key")
|
||||
for (0..i) |prior| {
|
||||
if mem.eql(u8, entry.0, entries[prior].0) compile_error!("duplicate static string map key")
|
||||
}
|
||||
|
||||
keys[i] = entry.0
|
||||
|
||||
Binary file not shown.
+13415
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user