Compare commits

...

104 Commits

Author SHA1 Message Date
hl-valdemar a74dd2e960 feat: support mutable metadata slice borrowing 2026-11-02 00:31:18 +01:00
hl-valdemar 2a4efd0b7f feat: support mixed runtime and metadata execution 2026-11-01 21:49:40 +01:00
hl-valdemar 22b40d3718 fix: propagate contextual compile-time execution 2026-11-01 19:48:51 +01:00
hl-valdemar 1f457d6b41 feat: support existing io.print formatting 2026-11-01 13:52:27 +01:00
hl-valdemar e036070bf2 feat: infer compile-time metadata locals 2026-11-01 11:03:39 +01:00
hl-valdemar 4fea18b834 feat: execute explicitly annotated compile-time metadata locals 2026-10-31 10:48:55 +01:00
hl-valdemar ee7eeac894 feat: execute value-returning compile-time metadata helpers 2026-10-31 03:34:11 +01:00
hl-valdemar ebae8e712c feat: execute tagged type-valued compile-time storage 2026-10-31 03:18:48 +01:00
hl-valdemar 4a1b30d9d5 feat: execute readonly type-valued compile-time slices 2026-10-31 02:44:39 +01:00
hl-valdemar 15cbebe40f feat: execute fallible type-valued compile-time storage 2026-10-30 20:24:26 +01:00
hl-valdemar 0625359d62 feat: execute optional type-valued compile-time storage
Preserve owned optional metadata snapshots, nested presence, captures, checked unwraps and erased specialization identity. Verify imported nominal ownership across repeated compilation. Bootstrap: 299 passed. Native: 1080 programs plus 5 allocator programs passed with a temporary 120-second command deadline; bounded inline expansion exceeds the harness default of 30 seconds.
2026-10-30 19:47:25 +01:00
hl-valdemar 83b6153e72 feat: construct partial compile-time type records 2026-10-30 18:36:40 +01:00
hl-valdemar 32a7a1de24 feat: materialize readonly typed compile-time slices 2026-10-30 18:03:36 +01:00
hl-valdemar cc39c96fc0 feat: materialize address-free compile-time untagged unions
Snapshot full union storage rather than choosing an overlapping field. Preserve structural union source syntax independently of nominal identity so direct literal arguments materialize and share normalized specialization keys. Reject uninitialized bytes and address-bearing member layouts.
2026-10-30 16:35:11 +01:00
hl-valdemar c8890b4355 feat: materialize nonzero-sentinel compile-time byte slices
Own nonzero trailing sentinels in canonical string-backed snapshots and preserve specialization identity across computed and direct views.

Verified: bro build; bro test (296 passed); python3 tests/native.py (1026 interpreter/native comparisons at -O0/-O2, 5 native allocator programs, LLVM output and CLI errors).
2026-10-30 15:48:54 +01:00
hl-valdemar 8e790b30cc feat: materialize readonly compile-time byte slices 2026-10-30 15:21:44 +01:00
hl-valdemar 6748b38eac feat: materialize computed compile-time callbacks 2026-10-30 14:23:12 +01:00
hl-valdemar c65f41db43 feat: infer generic arguments from expected results 2026-10-30 13:43:24 +01:00
hl-valdemar b1cc97cc77 refactor: use grouped matches for parser token membership 2026-10-30 12:42:38 +01:00
hl-valdemar e90d4f0829 feat: execute existing hash maps through Honey 2026-10-30 12:17:57 +01:00
hl-valdemar 31a1bf1995 refactor: remove enum-map lookup wrappers 2026-10-30 11:33:03 +01:00
hl-valdemar 30a98db379 refactor: precompute compiler enum mappings 2026-10-30 11:16:35 +01:00
hl-valdemar d7e637dc3b feat: support compound updates and bitwise operators 2026-10-30 09:59:44 +01:00
hl-valdemar 670ba8f756 feat: support yielding value blocks 2026-10-29 23:45:08 +01:00
hl-valdemar 9838b253ee feat: support optional-capture conditional expressions
Reuse canonical matches for optional presence and exact by-value payload captures across checking, lowering and compile-time selection.

Snapshot the optional-match flag before selector construction: AST additions can reallocate storage and invalidate the borrowed tree until refresh.

Verified 268 bootstrap tests and 908 interpreter/native programs at -O0 and -O2, plus native allocator, LLVM and CLI checks.
2026-10-29 22:53:59 +01:00
hl-valdemar 46fc7f4c46 feat: support boolean conditional expressions 2026-10-29 21:56:58 +01:00
hl-valdemar b832504f10 style: spell AST flag masks in binary 2026-10-29 21:46:36 +01:00
hl-valdemar 9ccb9be01f refactor: preserve compact AST node metadata 2026-10-29 21:01:13 +01:00
hl-valdemar 73684f856a feat: run existing ArrayList with imported C allocator 2026-10-29 20:51:59 +01:00
hl-valdemar 026fe36325 feat: support existing enum map initialization and lookup 2026-10-29 16:33:35 +01:00
hl-valdemar 6f892bbfcc feat: add compile-time type metadata reflection 2026-10-29 16:07:58 +01:00
hl-valdemar c6acff91fd feat: expand composed error-set inline match arms 2026-10-28 13:56:28 +01:00
hl-valdemar 5cf9e15661 feat: expand nominal inline match arms 2026-10-28 13:08:17 +01:00
hl-valdemar dc7b2e40e2 feat: expand heterogeneous inline tuples 2026-10-28 12:36:48 +01:00
hl-valdemar 20539ab455 feat: expand constant inline for loops 2026-10-28 12:14:42 +01:00
hl-valdemar 741ebbf484 feat: generate records from compile-time collections 2026-10-28 04:20:23 +01:00
hl-valdemar 440ef93a23 feat: pass type-valued records across compile-time procedures 2026-10-28 03:09:14 +01:00
hl-valdemar ae5314462a feat: execute initialized compile-time type records
Recognize type-bearing storage syntactically so recursive nominal fields expand only after instance caching.
2026-10-28 01:17:36 +01:00
hl-valdemar 443de49d55 feat: construct uninitialized compile-time value arrays 2026-10-28 01:07:16 +01:00
hl-valdemar ca53dab86b feat: construct uninitialized compile-time type arrays 2026-10-27 18:24:12 +01:00
hl-valdemar e00a2abe27 feat: implement compile-time field selection reflection 2026-10-27 16:51:55 +01:00
hl-valdemar 88b51274df feat: pass type-valued arrays across compile-time procedure boundaries 2026-10-27 16:15:22 +01:00
hl-valdemar 80867825e3 feat: execute type-valued arrays in type-producing procedures 2026-10-27 15:53:37 +01:00
hl-valdemar 0aca64a77e feat: execute collection and index iteration 2026-10-27 15:24:19 +01:00
hl-valdemar 0f5d3c222a feat: execute aggregate updates in type-producing procedures 2026-10-27 13:31:48 +01:00
hl-valdemar 08b171d3ff feat: execute optional captures in type-producing procedures 2026-10-27 13:17:08 +01:00
hl-valdemar c3e10b3bed feat: specialize constant matches in generic procedures
Reuse checked match selection for generic constant subjects and explicit match $(). Preserve owned captures, runtime scope and control transfers; keep ordinary calls, mutable subjects and potential runtime defaults unprobed. Update README and TODO checkpoint boundaries.

Verified: bro build; bro test (250 passed); python3 tests/native.py (555 interpreter/native comparisons at -O0/-O2, allocator, LLVM and CLI checks); direct interpreter/native constant-match smoke (42).
2026-10-27 13:06:11 +01:00
hl-valdemar 8d1a98f982 style: compact inner groups and simple guard bodies 2026-10-27 12:50:30 +01:00
hl-valdemar f062389ccc docs: allow concise bodies after multiline guards 2026-10-27 12:40:04 +01:00
hl-valdemar d5a351606d docs: allow compact inner expression grouping 2026-10-27 12:39:29 +01:00
hl-valdemar 84e3ed0cc8 style: align layout and ownership documentation 2026-10-27 11:15:36 +01:00
hl-valdemar 4a134b49dd docs: distinguish binding and pointee mutability 2026-10-27 11:07:18 +01:00
hl-valdemar 182bb3c3e0 feat: execute tagged-union matches in type procedures
Preserve resolver failures on synthetic selector captures before elaborating payloads; selector bodies omit the original capture uses.

Verified bro build, bro test (250 passed), and tests/native.py (536 interpreter/native programs at -O0 and -O2, allocator, LLVM and CLI checks).
2026-10-27 01:57:36 +01:00
hl-valdemar bf65f3c2c3 feat: materialize tagged-union and fallible constants 2026-10-27 01:41:06 +01:00
hl-valdemar e5215c4092 feat: execute nested compile-time type matches 2026-10-27 01:20:28 +01:00
hl-valdemar 85c60f2416 feat: execute compile-time matches in type procedures 2026-10-26 17:43:40 +01:00
hl-valdemar 8c174e8756 feat: execute mutable type-valued compile-time procedures 2026-10-26 17:18:54 +01:00
hl-valdemar 303a21a8f5 feat: specialize constant-input type-producing procedures 2026-10-26 16:49:19 +01:00
hl-valdemar 565733666e feat: specialize compile-time conditionals and type comparisons 2026-10-26 16:33:20 +01:00
hl-valdemar 4de7c8bfc3 feat: evaluate compile-time aggregates and explicit expressions 2026-10-26 16:14:05 +01:00
hl-valdemar ecd20675ea feat: evaluate compile-time scalar procedure calls 2026-10-26 15:34:38 +01:00
hl-valdemar 6886d743b5 feat: specialize generic type and value parameters 2026-10-26 14:45:14 +01:00
hl-valdemar 9058135326 feat: support fallible results and scoped cleanup 2026-10-26 13:01:55 +01:00
hl-valdemar cd64afbb40 feat: complete memory representation gate 2026-10-26 12:37:03 +01:00
hl-valdemar 7856f4f963 feat: support callback signatures and many-pointer addition 2026-10-26 11:46:40 +01:00
hl-valdemar 676f3666b4 feat: support source pointers and references 2026-10-26 11:32:22 +01:00
hl-valdemar 9966a1f523 feat: support enums, distinct types, unions and matching 2026-10-26 10:36:32 +01:00
hl-valdemar 280059d83d feat: add records and tuples across execution paths 2026-10-25 23:14:32 +01:00
hl-valdemar 3e93c6f000 feat: add integer widths and checked numeric conversions 2026-10-25 22:28:38 +01:00
hl-valdemar 0385e46295 test: complete scalar-function package acceptance 2026-10-25 22:03:33 +01:00
hl-valdemar c40d09b5cc test: use multiline diagnostic fixtures 2026-10-25 21:54:43 +01:00
hl-valdemar ac7ab63b9e test: lock down dependency diagnostic provenance 2026-10-25 21:40:42 +01:00
hl-valdemar d834ff9587 feat: resolve file-local import namespaces 2026-10-25 19:31:04 +01:00
hl-valdemar abd85e531b refactor: address package loader review feedback 2026-10-25 18:15:46 +01:00
hl-valdemar 69eb3ecba2 feat: load canonical directory packages 2026-10-25 17:54:50 +01:00
hl-valdemar a6d6a5b0a8 feat: retain compilation source and package identities 2026-10-25 15:31:50 +01:00
hl-valdemar e54a51327c docs: map self-hosting prerequisites 2026-10-25 03:47:51 +01:00
hl-valdemar 45359c0cdf feat: emit LLVM IR and build native executables 2026-10-25 02:23:02 +01:00
hl-valdemar c01d935a7b refactor: centralize IR construction invariants 2026-10-25 02:24:06 +02:00
hl-valdemar 2c11772931 refactor: expose checked function fallthrough obligations 2026-10-25 02:02:44 +02:00
hl-valdemar 2761c77182 refactor: use contextual empty slice literals 2026-10-25 01:53:35 +02:00
hl-valdemar 678bb2583b refactor: address interning review feedback 2026-10-25 00:41:39 +02:00
hl-valdemar 675fa5049f refactor: integrate compilation-owned identifier interning 2026-10-25 00:26:18 +02:00
hl-valdemar 0593786553 feat: parse floating-point text through the standard library 2026-10-24 04:08:58 +02:00
hl-valdemar 5fd8d9eb89 fix: reject inexact integer literals in float context 2026-10-24 03:55:53 +02:00
hl-valdemar 9c1bd2a20e fix: use composed enum keys for driver diagnostics (EnumMap) 2026-10-24 03:54:23 +02:00
hl-valdemar d2cec370fa perf: separate checker union-find links from node metadata 2026-10-24 03:48:27 +02:00
hl-valdemar c6a56b7e79 feat: execute fixed arrays and indexed access 2026-10-24 03:39:25 +02:00
hl-valdemar cf39481bfc feat: execute Bro-compatible integer range loops 2026-10-24 03:27:52 +02:00
hl-valdemar d51ca57c9e feat: execute break and continue in while loops 2026-10-24 03:22:05 +02:00
hl-valdemar 0add5ab187 fix: allow multiline parenthesized expressions in every context 2026-10-24 03:07:51 +02:00
hl-valdemar 5f845b9520 feat: execute short-circuit boolean expressions 2026-10-24 03:01:47 +02:00
hl-valdemar 7551ce5f6d refactor: query diagnostic message table directly 2026-10-24 02:50:00 +02:00
hl-valdemar e9478dcf3f refactor: store diagnostic messages in a static enum map 2026-10-24 02:40:15 +02:00
hl-valdemar b35a68dff8 feat: execute local assignments and while loops 2026-10-24 02:34:58 +02:00
hl-valdemar 021cbecac8 refactor: name recovery boundaries and compact IR strings 2026-10-24 02:15:50 +02:00
hl-valdemar 9932750e21 feat: execute conditionals through typed IR
Use typed singleton arrays for braceless arm blocks: &[statement] compiled but trapped under the Bro bootstrap.
2026-10-24 01:47:13 +02:00
hl-valdemar 0821b84bc3 refactor: index preprocessed process arguments directly 2026-10-23 12:11:42 +02:00
hl-valdemar c1e03632ed refactor: obtain arguments through process initialization 2026-10-23 11:39:27 +02:00
hl-valdemar 877ee21579 feat: restore sentinel-slice file path interfaces 2026-10-23 11:22:41 +02:00
hl-valdemar 31c2cad9f9 feat: add bounded read-to-end and compatible file paths 2026-10-23 11:02:11 +02:00
hl-valdemar 98f3a04daf feat: lower typed programs and interpret IR 2026-10-22 22:20:50 +02:00
hl-valdemar bc318a29ad fix: infer float context from division 2026-10-22 11:53:04 +02:00
hl-valdemar bbf39c16c5 feat: type-check expressions and function calls 2026-10-09 22:35:25 +02:00
62 changed files with 52707 additions and 559 deletions
+323 -5
View File
@@ -1,6 +1,6 @@
# Honey
Honey is currently bootstrapped with the Bro compiler. the expected development workspace places the Bro project at [`../brolang`](../brolang); consult that project for compiler behavior and documentation.
Bro and Honey implement the same language; Bro is the prototype compiler used to bootstrap the self-hosted Honey compiler. the features below describe Honey's currently implemented subset. the expected development workspace places Bro at [`../brolang`](../brolang); consult that project for the language grammar and documentation.
style: see the [language guidelines](docs/style.md).
@@ -10,10 +10,328 @@ build Honey from this directory:
bro build
```
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses.
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.
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 a trailing comma; newlines remain statement boundaries. argument/type checking belongs to semantic analysis and is not yet implemented.
run a source file with the interpreter:
name resolution (`@source/sema`) runs after parsing. it predeclares package names and function-body locals, allowing forward references and mutually recursive functions. parameters and locals share a function scope; signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `string`, `void`); sibling function scopes may independently reuse names.
```sh
./build/honey run example.hon
```
`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. type checking and read-before-initialization checks remain separate, unimplemented stages.
`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
}
main proc() int {
answer :: add(19, 23)
return answer
}
```
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`.
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. groups may span lines in any expression position, including returns, initializers, arguments and guards. parsing accepts integer division syntax, but type checking requires float operands for `/`.
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`.
`if`/`else` supports braced arms, single statements and `else if` chains. conditions must have type `bool`. a braceless guard requires parentheses unless it is a function call: `if (n <= 1) return 1` or `if ready() return`. `else` binds to the nearest `if`; after a braced arm it may start on the next line. branches are represented as AST blocks even when braceless.
`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 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.
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.
`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; sibling scopes may independently reuse names.
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.
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 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.
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.
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.
`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.
+198
View File
@@ -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.
+2 -2
View File
@@ -1,3 +1,3 @@
# compile through recoverable source errors
# execute through recoverable source errors
Honey produces an executable whenever source errors can be represented safely: structurally valid syntax trees may contain typed invalid nodes carrying source spans and diagnostic identities, and semantic checking or lowering turns demanded invalid code into diagnostic runtime traps. clean compilation returns status `0`, compilation that emits an executable with source diagnostics returns `1`, and unrecoverable infrastructure or backend failure returns `2`. this deliberately favors testing unaffected program paths over conventional fail-fast compilation; parser recovery must preserve valid surrounding syntax, while compact syntax-tree storage remains private behind typed views.
Honey lowers recoverable source errors into typed, target-independent IR with source spans and diagnostic-bearing trap terminators; the interpreter executes that IR first, and future native backends reuse the same lowering rather than duplicating frontend semantics. invalid code traps only when execution reaches it, allowing unaffected program paths to run; parser recovery preserves surrounding syntax and compact syntax-tree storage remains private behind typed views. clean execution returns status `0`, source diagnostics or runtime traps return `1` even when an unaffected entry point finishes successfully, and usage or unrecoverable infrastructure failure returns `2`.
+4 -3
View File
@@ -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
+37
View File
@@ -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
+16
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
main proc() int {
values [4]mut i64 := [1, 2, 3, 4]
values[2] = 10
total := 0
count usize :: 4
for 0..count |index| {
total = total + values[index]
}
return total
}
+11
View File
@@ -0,0 +1,11 @@
in_range proc(value, low, high int) bool {
return value >= low and value < high
}
main proc() bool {
return (
in_range(5, 0, 10)
and !in_range(12, 0, 10)
or in_range(20, 0, 10)
)
}
+8
View File
@@ -0,0 +1,8 @@
factorial proc(n int) int {
if (n <= 1) return 1
return n * factorial(n - 1)
}
main proc() int {
return factorial(5)
}
+11
View File
@@ -0,0 +1,11 @@
main proc() int {
product := 1
remaining := 5
while remaining > 1 {
product = product * remaining
remaining = remaining - 1
}
return product
}
+13
View File
@@ -0,0 +1,13 @@
main proc() int {
number := 0
total := 0
while true {
number = number + 1
if (number == 3) continue
if (number > 5) break
total = total + number
}
return total
}
+10
View File
@@ -0,0 +1,10 @@
main proc() int {
total := 0
for 0..10 |i| {
if (i == 5) continue
total = total + i
}
return total
}
+6
View File
@@ -0,0 +1,6 @@
to_fahrenheit proc(celsius float) float { return celsius * 9 / 5 + 32 }
main proc() float {
temperature :: 20
return to_fahrenheit(temperature)
}
+1899 -57
View File
File diff suppressed because it is too large Load Diff
+136
View File
@@ -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)
@@ -32,3 +39,132 @@ exposes_typed_declaration_views test {
try testing.expect_equal(ExprKind.literal_int, expr_kind(&tree, statement.initializer))
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_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]
literal :: try add_array_literal(&builder, span, elements[..])
empty :: try add_array_literal(&builder, span, &[])
indexed :: try add_index(&builder, span, literal, count)
package :: try add_package(&builder, span, &[])
tree := finish(&builder, package)
defer deinit(&tree)
try testing.expect_equal(ExprKind.array_type, expr_kind(&tree, matrix))
try testing.expect_equal(null, array_type(&tree, matrix).count)
try testing.expect(array_type(&tree, matrix).mutable)
try testing.expect_equal(row, array_type(&tree, matrix).element)
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))
try testing.expect_equal(element, array_element(&tree, literal, 1))
try testing.expect_equal(0, array_element_count(&tree, empty))
try testing.expect_equal(ExprKind.index, expr_kind(&tree, indexed))
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)
}
}
+124 -2
View File
@@ -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")
}
}
@@ -41,8 +47,14 @@ render_function proc(
try append_text(renderer, " function ")
try append_text(renderer, ast.source_text(tree, function.name))
try append_text(renderer, "\n")
for 0..ast.block_statement_count(tree, function.body) |index| {
statement :: ast.block_statement(tree, function.body, index)
try render_block(renderer, tree, function.body, 2)
}
@hide:file
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, " ")
match ast.stmt_kind(tree, statement) {
.declaration: try render_statement(renderer, tree, statement, "")
.expression: {
@@ -51,6 +63,92 @@ render_function proc(
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression)))
try append_text(renderer, "\n")
}
.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| {
try append_text(renderer, " ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
}
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 ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, conditional.condition)))
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, " ")
try append_text(renderer, "else\n")
try render_block(renderer, tree, alternative, depth + 1)
}
}
.assignment_stmt: {
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)))
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: {
loop :: ast.while_stmt(tree, statement)
try append_text(renderer, "while ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, loop.condition)))
try append_text(renderer, "\n")
try render_block(renderer, tree, loop.body, depth + 1)
}
.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.index_capture) {
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)
}
.invalid: try append_text(renderer, "invalid statement\n")
}
}
@@ -67,6 +165,13 @@ render_statement proc(
try append_text(renderer, indent)
try append_text(renderer, "declaration ")
try append_text(renderer, ast.source_text(tree, statement.name))
if (statement.type) |type| {
try append_text(renderer, " ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, type)))
}
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")
}
@@ -74,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 = "<<|",
},
)
+45 -18
View File
@@ -1,27 +1,54 @@
import "@std/mem"
import "@std/testing"
import "@source/diagnostics"
import "@source/strpool"
import "@source/lexer"
import "@source/parser"
import "@source/ast"
renders_semantic_debug_view test {
builder := ast.init_builder(mem.c_allocator, "value :: 42")
defer ast.deinit_builder(&builder)
value :: try ast.add_literal(&builder, .int, ast.Span{ start = 9, end = 11 })
declaration :: try ast.add_statement_decl(
&builder,
ast.Span{ start = 0, end = 11 },
ast.Span{ start = 0, end = 5 },
null,
.constant,
value,
)
declarations [1]ast.NodeId :: [declaration]
package :: try ast.add_package(&builder, ast.Span{ start = 0, end = 11 }, declarations[..])
tree := ast.finish(&builder, package)
renders_array_annotations_literals_and_indexed_assignments test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`data [2]mut i64 := [1, 2]
`main proc() i64 {
` (data)[0] = 9
` return data[0]
`}
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(mem.eql(u8, "package\n declaration value\n", renderer.bytes.items))
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
try testing.expect_equal(
"package\n declaration data [2]mut i64 := [1, 2]\n function main\n assignment (data)[0] = 9\n return data[0]\n",
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))
}
+414
View File
@@ -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
+260 -7
View File
@@ -1,32 +1,149 @@
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,
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.
Store :: struct { list arraylist.ArrayList(Diagnostic) }
CapacityError :: enum { diagnostic_limit_reached }
CapacityError :: enum { diagnostic_limit_reached, storage_limit_reached }
#! retains `allocator` for added diagnostics; the caller owns the returned Store.
init proc(allocator mem.Allocator) Store {
@@ -35,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)
}
@@ -46,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)]
@@ -54,3 +190,120 @@ get proc(store @Store, id DiagnosticId) ?Diagnostic {
count proc(store @Store) usize {
return store.list.items.len
}
#! constructed at compile time so missing entries fail compilation; message bytes live for the process lifetime.
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",
expected_type = "annotation must name a type",
type_not_value = "type name cannot be used as a value",
invalid_void_type = "void cannot be stored or used as a parameter type",
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 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",
invalid_array_length = "array length must be a nonnegative supported compile-time integer",
array_length_mismatch = "array literal length does not match its type",
invalid_array_element_type = "array element annotations require a supported concrete value type",
array_size_overflow = "array storage size exceeds the supported address space",
invalid_index_type = "array index must have type usize",
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 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",
read_before_initialization = "local value was read before initialization",
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
File diff suppressed because it is too large Load Diff
+665
View File
@@ -0,0 +1,665 @@
import "@std/mem"
import "@std/arraylist"
import "@std/debug"
import "@std/enummap"
import "@source/diagnostics"
FunctionId :: distinct u32
GlobalId :: distinct u32
LocalId :: distinct u32
ValueId :: distinct u32
BlockId :: distinct u32
StringId :: distinct u32
ArrayId :: distinct u32
no_array :: maxval!(ArrayId)
no_value :: maxval!(ValueId)
Type :: enum {
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
string StringId
function FunctionId
elements []mut Value
address u32
}
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 }
}
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 {
kind Type = .array
element Type = .invalid
element_array ArrayId = no_array
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.
ArrayStorage :: struct {
index u32
array_type ArrayId
offset usize
local bool
}
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
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
}
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 {
return_value,
trap,
jump,
branch,
unreachable,
}
Terminator :: struct {
kind TerminatorKind = .return_value
value ValueId = no_value
target BlockId = BlockId(0)
alternative BlockId = BlockId(0)
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
Block :: struct {
instructions arraylist.ArrayList(Instruction)
arguments arraylist.ArrayList(ValueId)
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
blocks arraylist.ArrayList(Block)
entry BlockId = BlockId(0)
value_count usize = 0
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
Global :: struct {
type Type
array_type ArrayId = no_array
initializer FunctionId
span diagnostics.Span
}
#! retains the original allocation for freeing while exposing only decoded bytes.
StringStorage :: struct {
allocation []mut u8
len usize
}
#! owns executable storage, including decoded strings; no AST or binding table is retained.
#! spans and diagnostic IDs refer to the caller's source and diagnostic store.
Module :: struct {
allocator mem.Allocator
functions arraylist.ArrayList(Function)
globals arraylist.ArrayList(Global)
strings arraylist.ArrayList(StringStorage)
arrays arraylist.ArrayList(ArrayType)
entry ?FunctionId = null
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,
functions = arraylist.init(allocator),
globals = arraylist.init(allocator),
strings = arraylist.init(allocator),
arrays = arraylist.init(allocator),
}
}
#! 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),
locals = arraylist.init(allocator),
blocks = arraylist.init(allocator),
array_storage = arraylist.init(allocator),
}
}
#! 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),
arguments = arraylist.init(allocator),
}
}
#! 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)]
return string.allocation[..string.len]
}
#! releases all executable storage using the retained allocator.
deinit proc(module @mut Module) void {
for (0..(module.functions.items.len)) |i| {
function :: &module.functions.items[i]
for (0..(function.blocks.items.len)) |j| {
block :: &function.blocks.items[j]
arraylist.deinit(&block.instructions)
arraylist.deinit(&block.arguments)
}
arraylist.deinit(&function.parameters)
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)
arraylist.deinit(&module.arrays)
}
+61
View File
@@ -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,
}))
}
+220 -64
View File
@@ -1,6 +1,7 @@
import "@std/mem"
import "@std/strmap"
import "@std/arraylist"
import "@std/enummap"
import "@source/diagnostics"
import "@source/strpool"
@@ -15,23 +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.
@@ -39,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
}
@@ -69,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 },
})
@@ -102,36 +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] == '='
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(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
@@ -139,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
}
@@ -153,59 +228,125 @@ 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(source, cursor, cursor + 2),
kind = value,
})
cursor += 2
continue
}
}
kind ?TokenKind :: match char {
'=': .equal
'!': .bang
'~': .tilde
'<': .less
'>': .greater
',': .comma
'|': .pipe
'@': .at
'$': .dollar
'&': .ampersand
'^': .caret
'?': .question
';': .semicolon
'(': .open_paren
')': .close_paren
'[': .open_bracket
@@ -215,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
}
@@ -223,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
@@ -244,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 }
}
@@ -273,11 +417,14 @@ 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] == '.' {
if (
allow_decimal and cursor < program.len and program[cursor] == '.'
and !(cursor + 1 < program.len and program[cursor + 1] == '.')
) {
has_decimal = true
cursor += 1
}
@@ -295,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
@@ -331,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
+322 -10
View File
@@ -3,30 +3,342 @@ 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 {
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, &pool, &diagnostic_store)
expected [11]TokenKind :: [
TokenKind.true, .false, .ident, .ident,
.equal_equal, .not_equal, .less, .less_equal, .greater, .greater_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)
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
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(&pool, state.tokens.items[2].data.str_id)?)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
scans_logical_operators_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)
source :: "and or android orbit and_1 or2 ! != !! !true"
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(&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)
try testing.expect_equal(u32(35), state.tokens.items[7].span.end)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
scans_loop_controls_with_exact_keyword_boundaries 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 :: "break continue breakfast continued break_1 continue2"
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)
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
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(&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_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, "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,
.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)
try testing.expect_equal(u32(1), state.tokens.items[1].span.start)
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(1, diagnostics.count(&diagnostic_store))
}
scans_array_delimiters_and_mut_keyword 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, "[_]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,
.open_bracket, .int, .comma, .int, .close_bracket,
.open_bracket, .close_bracket, .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(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))
}
+38 -11
View File
@@ -20,10 +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
@@ -39,16 +58,30 @@ TokenKind :: enum {
plus, minus
star, slash
# bitwise
tilde
shift_left, shift_right, shift_left_saturating
range, range_inclusive, ellipsis
at, ampersand, caret, dollar
question, semicolon
pipe
# logical
bang
# 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
@@ -59,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 {
.ident, .string, .character, .int, .float:
debug.print("{}({})\n", {token.kind, program[token.span.start..token.span.end]})
}
}
.string, .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})
}
}
+761
View File
@@ -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
}
}
+668
View File
@@ -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")
}
+163
View File
@@ -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
`}
+183
View File
@@ -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
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+274 -51
View File
@@ -1,14 +1,15 @@
import "@std/debug"
import "@ffi/c"
import "@std/mem"
import "@std/arraylist"
import "@std/enummap"
import "@std/io"
import "@std/process"
import "@source/diagnostics"
import "@source/strpool"
import "@source/lexer"
import "@source/ast"
import "@source/parser"
import "@source/sema"
ast_renderer :: import "@source/ast/renderer"
import "@source/compilation"
import "@source/ir"
import "@source/interpreter"
import "@source/llvm"
test import "@std/mem"
test import "@std/enummap"
@@ -16,58 +17,280 @@ test import "@std/arraylist"
test import "@std/hashmap"
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"
test import "@source/lexer"
test import "@source/ast"
test import "@source/ast/renderer"
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
program ::
`# literals
`x float :: 123.9
`y int := 43
`z :: "hello"
`
`a := 1 + 2 * 3
`b :: -3 / 2 + 1
`
`grouped proc(left, right int, extra float) void {
` value int :: later + left
` later int :: right
` empty()
` grouped(value, later, extra)
`}
`empty proc() void {}
main proc() void! {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.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, program, &diagnostic_store)
tree := try parser.parse(
mem.c_allocator,
scan_state.tokens.items,
program,
&diagnostic_store,
DriverError :: alias (
compilation.Error
| io.OpenError
| io.CloseError
| io.ReadError
| io.WriteError
| process.RunError
| NativeBuildError
)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
@hide:file
NativeBuildError :: enum { temporary_file_failed, compiler_failed }
renderer := ast_renderer.init(mem.c_allocator)
defer ast_renderer.deinit(&renderer)
try ast_renderer.render(&renderer, &tree)
debug.print("{s}", {renderer.bytes.items})
debug.print("diagnostics: {}\n", {diagnostics.count(&diagnostic_store)})
main proc(init process.Init) i32 {
return command(mem.c_allocator, init.io, init.args) catch |err| {
if (init.args.values.len > 2) {
io.print("{s}: infrastructure error: {s}\n", io.stderr(init.io), {
init.args.values[2], enummap.get(&infrastructure_messages, err)?,
}) catch |_| {}
} else {
io.print("honey: infrastructure error: {s}\n", io.stderr(init.io), {
enummap.get(&infrastructure_messages, err)?,
}) catch |_| {}
}
return 2
}
}
@hide:file
infrastructure_messages :: $enummap.init(DriverError, []u8, {
out_of_memory = "out of memory",
diagnostic_limit_reached = "diagnostic limit reached",
storage_limit_reached = "aggregate storage limit reached",
source_too_large = "source file is too large",
out_of_space = "identifier storage is full",
open_failed = "cannot open source file",
close_failed = "cannot close source file",
not_open_for_reading = "source file is not readable",
read_failed = "cannot read source file",
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) {
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 (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)
}
}
try usage(io.stderr(runtime))
return 2
}
@hide:file
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 |_| {}
}
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)
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 {
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)
outcome := try interpreter.run(allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
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
}
@hide:file
print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError {
if (value.type == .void) return
try print_value(output, module, value)
try io.print("\n", output, {})
}
@hide:file
print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError {
match value.type {
.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)})
.pointer: try io.write_all(output, "<pointer>")
.array, .record: {
shape :: module.arrays.items[usize(value.array_type)]
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 io.write_all(output, closing)
return
}
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
}
}
@hide:file
report_diagnostics proc(
allocator mem.Allocator,
output io.Writer,
sources []diagnostics.Source,
diagnostic_store @diagnostics.Store,
trap ?diagnostics.DiagnosticId,
) void ! (mem.AllocError | io.WriteError) {
if (diagnostics.count(diagnostic_store) == 0) return
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.len
while (line + 1 < end) {
middle :: line + divtrunc!(end - line, 2)
if (starts[middle] <= offset) line = middle
else end = middle
}
label []u8 := "error"
if (trap) |id| {
if (id == diagnostics.DiagnosticId(index)) label = "runtime trap"
}
try io.print("{s}:{}:{}: {s}: {s}\n", output, {
source.path,
line + 1,
offset - starts[line] + 1,
label,
diagnostics.message(diagnostic),
})
}
}
}
+1932 -145
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+509 -51
View File
@@ -1,25 +1,85 @@
import "@std/debug"
import "@std/mem"
import "@std/hashmap"
import "@std/strmap"
import "@std/arraylist"
import "@source/strpool"
import "@source/ast"
import "@source/diagnostics"
BuiltinType :: enum { int, float, string, void }
BuiltinType :: enum {
int, float,
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 },
{ "string", .string },
{ "void", .void },
])
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
}
@@ -29,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
@@ -41,7 +109,7 @@ Resolution :: struct { storage Storage }
@hide:file
Scope :: struct {
names hashmap.StringHashMap(Binding)
names hashmap.AutoHashMap(strpool.StringId, Binding)
parent ?@Scope
}
@@ -50,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)
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, &package_scope, id, ast.statement_decl(tree, id).name)
.function: try predeclare(&state, &package_scope, id, ast.function_decl(tree, id).name)
.invalid: {}
.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 (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
hashmap.put(&state.namespaces, root, scope^) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
}
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 resolve_statement_decl(&state, &package_scope, id)
.function: try resolve_function(&state, &package_scope, id)
.invalid: resolve_invalid(&state, 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,
} }
}
@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: {}
}
}
}
return Resolution{ storage = Storage{ bindings = bindings, allocator = allocator } }
@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.
@@ -105,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(&current.names, name) |found| return found
return lookup(current.parent, name)
return lookup(state, current.parent, name)
}
for (state.builtin_ids) |id, index| {
if (id == name) return Binding{
kind = .builtin,
data = BindingData{ builtin = builtins[index].type },
}
if strmap.get(&builtins, name) |type| {
return Binding{ kind = .builtin, data = BindingData{ builtin = 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
@@ -134,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{
@@ -145,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 } }
@@ -162,36 +365,100 @@ 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)
try predeclare(state, &scope, parameter, ast.param(tree, parameter).name)
}
for (0..ast.block_statement_count(tree, function.body)) |i| {
statement :: ast.block_statement(tree, function.body, i)
if ast.stmt_kind(tree, statement) == .declaration {
try predeclare(state, &scope, statement, ast.statement_decl(tree, statement).name)
}
try resolve_block(state, &scope, function.body)
}
for (0..ast.block_statement_count(tree, function.body)) |i| {
statement :: ast.block_statement(tree, function.body, i)
@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,
}
defer hashmap.deinit(&scope.names)
try resolve_block(state, &scope, id)
}
@hide:file
resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
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)
}
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
match ast.stmt_kind(tree, statement) {
.declaration: try resolve_statement_decl(state, &scope, statement)
.expression: try resolve_expr(state, &scope, ast.statement_expr(tree, statement))
.declaration: try resolve_statement_decl(state, scope, statement)
.expression: try resolve_expr(state, scope, ast.statement_expr(tree, statement))
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
}
.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_capture_arm(state, scope, conditional.then_block, conditional.capture)
if (conditional.else_block) |other| try resolve_arm(state, scope, other)
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(tree, statement)
try resolve_expr(state, scope, assignment.target)
try resolve_expr(state, scope, assignment.value)
}
.while_stmt: {
loop :: ast.while_stmt(tree, statement)
try resolve_expr(state, scope, loop.condition)
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,
}
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))
try resolve_block(state, &body_scope, loop.body)
}
.invalid: resolve_invalid(state, statement)
}
}
}
@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,
@@ -199,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
@@ -222,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_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)
@@ -236,18 +612,100 @@ 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)
}
.range: {
expression :: ast.range_expr(tree, id)
try resolve_expr(state, scope, expression.start)
try resolve_expr(state, scope, expression.end)
}
.call: {
try resolve_expr(state, scope, ast.call_expr(tree, id).callee)
for (0..ast.call_arg_count(tree, id)) |i| {
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)
try resolve_expr(state, scope, type.element)
}
.array_literal: {
for (0..ast.array_element_count(tree, id)) |i| {
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)
try resolve_expr(state, scope, expression.index)
}
.invalid: resolve_invalid(state, id)
}
}
+686 -12
View File
@@ -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]
@@ -298,3 +298,677 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
try testing.expect_equal(later, binding(&resolution, valid_ref).data.declaration)
try testing.expect_equal(syntax_count + 3, diagnostics.count(&diagnostic_store))
}
resolves_conditional_arm_scopes_without_leaks_or_shadowing test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`choose proc(flag bool, value int) void {
` if flag {
` before :: later
` later :: value
` if before == value { nested :: later }
` } else {
` later :: value
` }
` later
` if later { later :: 1 }
` if flag {
` value :: 2
` bool :: false
` if flag { value }
` }
`}
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)
function :: ast.package_decl(&tree, 0)
view :: ast.function_decl(&tree, function)
flag :: ast.function_param(&tree, function, 0)
value :: ast.function_param(&tree, function, 1)
try testing.expect_equal(BuiltinType.bool, binding(&resolution, ast.param(&tree, flag).type).data.builtin)
first :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 0))
try testing.expect_equal(flag, binding(&resolution, first.condition).data.declaration)
before :: ast.block_statement(&tree, first.then_block, 0)
later :: ast.block_statement(&tree, first.then_block, 1)
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, before).initializer).data.declaration)
try testing.expect_equal(value, binding(&resolution, ast.statement_decl(&tree, later).initializer).data.declaration)
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, first.then_block, 2))
nested_decl :: ast.block_statement(&tree, nested.then_block, 0)
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, nested_decl).initializer).data.declaration)
if (first.else_block) |other| {
sibling :: ast.block_statement(&tree, other, 0)
try testing.expect_equal(BindingKind.declaration, binding(&resolution, sibling).kind)
try testing.expect_equal(sibling, binding(&resolution, sibling).data.declaration)
} else {
return .expectation_failed
}
outside :: ast.statement_expr(&tree, ast.block_statement(&tree, view.body, 1))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, outside).kind)
independent :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 2))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, independent.condition).kind)
try testing.expect_equal(BindingKind.declaration, binding(&resolution, ast.block_statement(&tree, independent.then_block, 0)).kind)
forbidden :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 3))
shadow :: ast.block_statement(&tree, forbidden.then_block, 0)
builtin :: ast.block_statement(&tree, forbidden.then_block, 1)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, shadow).kind)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind)
poisoned :: ast.if_stmt(&tree, ast.block_statement(&tree, forbidden.then_block, 2))
use :: ast.statement_expr(&tree, ast.block_statement(&tree, poisoned.then_block, 0))
try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, use).data.diagnostic)
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
}
resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc(flag bool) void {
` outer := 2
` while flag {
` later = outer
` later := 1
` while later > 0 {
` outer = later
` inner := later
` }
` inner
` }
` while flag { later := 3 }
` later = unknown_rhs
` while flag {
` outer := 1
` outer = 2
` bool := false
` }
`}
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)
function :: ast.package_decl(&tree, 0)
body :: ast.function_decl(&tree, function).body
flag :: ast.function_param(&tree, function, 0)
outer :: ast.block_statement(&tree, body, 0)
first :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1))
try testing.expect_equal(flag, binding(&resolution, first.condition).data.declaration)
forward :: ast.assignment_stmt(&tree, ast.block_statement(&tree, first.body, 0))
later :: ast.block_statement(&tree, first.body, 1)
try testing.expect_equal(BindingKind.declaration, binding(&resolution, forward.target).kind)
try testing.expect_equal(later, binding(&resolution, forward.target).data.declaration)
try testing.expect_equal(outer, binding(&resolution, forward.value).data.declaration)
nested :: ast.while_stmt(&tree, ast.block_statement(&tree, first.body, 2))
guard :: ast.binary_expr(&tree, nested.condition)
try testing.expect_equal(later, binding(&resolution, guard.left).data.declaration)
update :: ast.assignment_stmt(&tree, ast.block_statement(&tree, nested.body, 0))
try testing.expect_equal(outer, binding(&resolution, update.target).data.declaration)
try testing.expect_equal(later, binding(&resolution, update.value).data.declaration)
inner :: ast.block_statement(&tree, nested.body, 1)
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, inner).initializer).data.declaration)
escaped_inner :: ast.statement_expr(&tree, ast.block_statement(&tree, first.body, 3))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped_inner).kind)
sibling :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 2))
sibling_local :: ast.block_statement(&tree, sibling.body, 0)
try testing.expect_equal(BindingKind.declaration, binding(&resolution, sibling_local).kind)
try testing.expect(sibling_local != later)
escaped :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 3))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped.target).kind)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped.value).kind)
try testing.expect(binding(&resolution, escaped.target).data.diagnostic != binding(&resolution, escaped.value).data.diagnostic)
forbidden :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 4))
shadow :: ast.block_statement(&tree, forbidden.body, 0)
poisoned :: ast.assignment_stmt(&tree, ast.block_statement(&tree, forbidden.body, 1))
builtin :: ast.block_statement(&tree, forbidden.body, 2)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, shadow).kind)
try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, poisoned.target).data.diagnostic)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind)
try testing.expect_equal(5, diagnostics.count(&diagnostic_store))
}
resolves_all_code_and_preserves_scopes_after_loop_controls test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` while (true) {
` break
` before :: later
` continue
` later :: 1
` missing_after_break
` if (true) {
` continue
` private :: later
` int :: 2
` }
` private
` }
` later
`}
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(4, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0))
before :: ast.block_statement(&tree, loop.body, 1)
later :: ast.block_statement(&tree, loop.body, 3)
reference :: ast.statement_decl(&tree, before).initializer
try testing.expect_equal(BindingKind.declaration, binding(&resolution, reference).kind)
try testing.expect_equal(later, binding(&resolution, reference).data.declaration)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 5))
private_decl :: ast.block_statement(&tree, conditional.then_block, 1)
private_ref :: ast.statement_decl(&tree, private_decl).initializer
try testing.expect_equal(later, binding(&resolution, private_ref).data.declaration)
forbidden :: ast.block_statement(&tree, conditional.then_block, 2)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, forbidden).kind)
}
range_bounds_use_outer_scope_and_captures_are_body_declarations test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`run proc() void {
` for lower..upper |item| {
` before :: later
` later :: item
` continue
` item
` }
` for lower..upper |item| { item }
` lower :: 0
` upper :: 2
` item
`}
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)
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 0))
sibling :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 1))
lower :: ast.block_statement(&tree, body, 2)
upper :: ast.block_statement(&tree, body, 3)
bounds :: ast.range_expr(&tree, loop.iterable)
try testing.expect_equal(lower, binding(&resolution, bounds.start).data.declaration)
try testing.expect_equal(upper, binding(&resolution, bounds.end).data.declaration)
if (loop.capture) |capture| {
try testing.expect_equal(capture, binding(&resolution, capture).data.declaration)
later :: ast.block_statement(&tree, loop.body, 1)
before_ref :: ast.statement_decl(&tree, ast.block_statement(&tree, loop.body, 0)).initializer
try testing.expect_equal(later, binding(&resolution, before_ref).data.declaration)
capture_ref :: ast.statement_decl(&tree, later).initializer
try testing.expect_equal(capture, binding(&resolution, capture_ref).data.declaration)
dead_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 3))
try testing.expect_equal(capture, binding(&resolution, dead_ref).data.declaration)
} else return .expectation_failed
if (sibling.capture) |capture| {
try testing.expect_equal(BindingKind.declaration, binding(&resolution, capture).kind)
sibling_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, sibling.body, 0))
try testing.expect_equal(capture, binding(&resolution, sibling_ref).data.declaration)
} else return .expectation_failed
escaped :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 4))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped).kind)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
range_captures_preserve_duplicate_builtin_and_discard_errors test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`run proc() void {
` outer :: 0
` for outer..2 |outer| { outer }
` for 0..2 |int| { int }
` for 0..2 |item, item| { item }
` for 0..2 |local| {
` local :: 1
` local
` }
` for 0..2 |_| {
` break
` _
` missing
` }
` for missing_start..missing_end |i| {}
`}
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)
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
for (usize(1)..usize(3)) |index| {
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, index))
if (loop.capture) |capture| {
try testing.expect_equal(BindingKind.invalid, binding(&resolution, capture).kind)
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 0))
try testing.expect_equal(
binding(&resolution, capture).data.diagnostic,
binding(&resolution, reference).data.diagnostic,
)
} 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.capture) |capture| {
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, indexed.body, 0))
try testing.expect_equal(capture, binding(&resolution, reference).data.declaration)
} else return .expectation_failed
local_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 4))
duplicate :: ast.block_statement(&tree, local_loop.body, 0)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, duplicate).kind)
discard :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 5))
try testing.expect(discard.capture == null)
for (usize(1)..usize(3)) |index| {
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, discard.body, index))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, reference).kind)
}
missing_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 6))
bounds :: ast.range_expr(&tree, missing_loop.iterable)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, bounds.start).kind)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, bounds.end).kind)
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
}
resolves_array_counts_elements_indices_and_unreachable_targets test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`size usize :: 2
`make proc(first, second [size]mut [2]i64) [size]i64 {
` return first[0]
` dead [local_size]mut [size - 1]f64 := [[later]]
` local_size :: size
` later f64 :: 1.0
` (dead[index])[0] = later
` index usize :: 0
`}
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))
size :: ast.package_decl(&tree, 0)
function :: ast.package_decl(&tree, 1)
view :: ast.function_decl(&tree, function)
return_type :: ast.array_type(&tree, view.return_type)
try testing.expect_equal(size, binding(&resolution, return_type.count?).data.declaration)
try testing.expect_equal(BuiltinType.i64, binding(&resolution, return_type.element).data.builtin)
first :: ast.function_param(&tree, function, 0)
second :: ast.function_param(&tree, function, 1)
first_type :: ast.param(&tree, first).type
try testing.expect_equal(first_type, ast.param(&tree, second).type)
parameter_array :: ast.array_type(&tree, first_type)
try testing.expect_equal(size, binding(&resolution, parameter_array.count?).data.declaration)
row :: ast.array_type(&tree, parameter_array.element)
try testing.expect_equal(BuiltinType.i64, binding(&resolution, row.element).data.builtin)
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, view.body, 0)).value?
try testing.expect_equal(first, binding(&resolution, ast.index_expr(&tree, returned).base).data.declaration)
dead :: ast.block_statement(&tree, view.body, 1)
local_size :: ast.block_statement(&tree, view.body, 2)
later :: ast.block_statement(&tree, view.body, 3)
index :: ast.block_statement(&tree, view.body, 5)
declaration :: ast.statement_decl(&tree, dead)
array :: ast.array_type(&tree, declaration.type?)
try testing.expect_equal(local_size, binding(&resolution, array.count?).data.declaration)
inner :: ast.array_type(&tree, array.element)
count_expression :: ast.binary_expr(&tree, inner.count?)
try testing.expect_equal(size, binding(&resolution, count_expression.left).data.declaration)
try testing.expect_equal(BuiltinType.f64, binding(&resolution, inner.element).data.builtin)
literal_row :: ast.array_element(&tree, declaration.initializer, 0)
try testing.expect_equal(later, binding(
&resolution, ast.array_element(&tree, literal_row, 0),
).data.declaration)
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, view.body, 4))
target :: ast.index_expr(&tree, ast.index_expr(&tree, assignment.target).base)
try testing.expect_equal(dead, binding(&resolution, target.base).data.declaration)
try testing.expect_equal(index, binding(&resolution, target.index).data.declaration)
try testing.expect_equal(later, binding(&resolution, assignment.value).data.declaration)
try testing.expect_equal(BuiltinType.usize, binding(
&resolution, ast.statement_decl(&tree, size).type?,
).data.builtin)
}
array_signature_counts_use_package_scope_and_local_counts_see_parameters test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`size :: 2
`inspect proc(n usize, a, b [n]i64) [body_size]i64 {
` body_size :: size
` local [n]i64 :: []
` return []
`}
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))
function :: ast.package_decl(&tree, 1)
view :: ast.function_decl(&tree, function)
parameter :: ast.function_param(&tree, function, 0)
grouped_type :: ast.param(&tree, ast.function_param(&tree, function, 1)).type
try testing.expect_equal(grouped_type, ast.param(
&tree, ast.function_param(&tree, function, 2),
).type)
signature_count :: ast.array_type(&tree, grouped_type).count?
try testing.expect_equal(BindingKind.invalid, binding(&resolution, signature_count).kind)
result_count :: ast.array_type(&tree, view.return_type).count?
try testing.expect_equal(BindingKind.invalid, binding(&resolution, result_count).kind)
local :: ast.statement_decl(&tree, ast.block_statement(&tree, view.body, 1))
local_count :: ast.array_type(&tree, local.type?).count?
try testing.expect_equal(BindingKind.declaration, binding(&resolution, local_count).kind)
try testing.expect_equal(parameter, binding(&resolution, local_count).data.declaration)
try testing.expect_equal(BuiltinType.i64, binding(
&resolution, ast.array_type(&tree, local.type?).element,
).data.builtin)
}
resolves_every_array_child_error_and_preserves_lexer_diagnostics test {
pool := strpool.init(mem.c_allocator)
defer strpool.deinit(&pool)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` return
` data [missing_count]MissingElement :: [absent, missing_element]
` (missing_base[missing_index])[missing_outer] = missing_rhs
` malformed [%]i64 :: [%]
`}
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(2, diagnostics.count(&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
declaration :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 1))
type :: ast.array_type(&tree, declaration.type?)
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 2))
outer :: ast.index_expr(&tree, assignment.target)
inner :: ast.index_expr(&tree, outer.base)
unknowns [8]ast.NodeId :: [
type.count?, type.element,
ast.array_element(&tree, declaration.initializer, 0),
ast.array_element(&tree, declaration.initializer, 1),
inner.base, inner.index, outer.index, assignment.value,
]
for (unknowns) |id| {
result :: binding(&resolution, id)
try testing.expect_equal(BindingKind.invalid, result.kind)
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostics.get(
&diagnostic_store, result.data.diagnostic,
)?.code)
}
malformed :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3))
malformed_count :: ast.array_type(&tree, malformed.type?).count?
malformed_element :: ast.array_element(&tree, malformed.initializer, 0)
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
+506
View File
@@ -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,
)
}
+1 -3
View File
@@ -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
+16
View File
@@ -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"))
}
+6 -13
View File
@@ -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
+24 -2
View File
@@ -1,5 +1,7 @@
import "@std/meta"
#! keys may be enums or native enum compositions that normalize to an enum, including aliases.
#! shared variant names follow native composition identity; payload-bearing sums are not enum keys.
EnumMap proc($E, $V type) type {
match typeinfo!(E) {
.enum |info|: return struct {
@@ -13,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
@@ -23,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")
}
@@ -36,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
}
+77
View File
@@ -24,3 +24,80 @@ handles_sparse_enum_get test {
try testing.expect_type(?[]u8, eof)
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
left
absent_left
}
@hide:file
TestRightError :: enum {
shared
right
absent_right
}
@hide:file
TestNestedError :: alias (TestLeftError | (TestRightError | TestLeftError))
@hide:file
TestComposedError :: alias TestNestedError
@hide:file
composed_values :: $init(TestComposedError, i32, {
shared = 11,
left = 22,
absent_left = 33,
right = 44,
absent_right = 55,
})
handles_composed_enum_keys_at_runtime test {
names EnumMap(TestComposedError, []u8) :: init({
shared = "shared error",
left = "left error",
right = "right error",
})
keys [_]TestComposedError :: [
TestLeftError.shared,
TestLeftError.left,
TestRightError.shared,
TestRightError.right,
TestLeftError.absent_left,
TestRightError.absent_right,
]
expected [_]?[]u8 :: [
"shared error",
"left error",
"shared error",
"right error",
null,
null,
]
expected_values [_]i32 :: [11, 22, 11, 44, 33, 55]
for keys |key, index| {
value :: get(&names, key)
try testing.expect_type(?[]u8, value)
try testing.expect_equal(expected[index], value)
try testing.expect_equal(expected_values[index], get(&composed_values, key)?)
}
}
+150
View File
@@ -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
}
+28
View File
@@ -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
View File
@@ -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")
}
}
+75
View File
@@ -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],
}))
}
+1 -1
View File
@@ -19,7 +19,7 @@ CloseError :: enum {
close_failed
}
#! open a caller-owned handle; release it with close.
#! open a caller-owned handle from a borrowed zero-terminated path; release it with close.
open proc(io Io, path [;0]u8, mode FileMode) File ! OpenError {
handle Handle :: try io.vtable.open(io.context, path, mode)
return File{ io = io, handle = handle }
+42 -19
View File
@@ -1,5 +1,6 @@
import "@ffi/c"
import "@std/mem"
import "@std/arraylist"
# typeinfo! requires this import even without a qualified meta reference.
import "@std/meta"
@@ -8,6 +9,12 @@ ReadError :: enum {
read_failed
}
ReadLimitError :: enum {
limit_exceeded
}
ReadAllError :: alias (mem.AllocError | ReadError | ReadLimitError)
WriteError :: enum {
not_open_for_writing
write_failed
@@ -64,6 +71,34 @@ read proc(input Reader, buffer []mut u8) usize ! ReadError {
return count
}
#! read to EOF, accepting at most `maximum` bytes; the reader remains caller-owned.
#! exceeding the limit consumes one extra byte and returns limit_exceeded.
#! returned storage retains `allocator`; release it with arraylist.deinit.
#! all allocated storage is released on error.
read_all proc(allocator mem.Allocator, input Reader, maximum usize) arraylist.ArrayList(u8) ! ReadAllError {
bytes := arraylist.init(u8, allocator)
errdefer arraylist.deinit(&bytes)
while true {
length :: bytes.items.len
if (length == maximum) {
probe [1]mut u8 := undefined
if (try read(input, (&probe)[..]) != 0) return .limit_exceeded
return bytes
}
if (length == bytes.capacity) {
growth := usize(4096)
if (growth > maximum - length) growth = maximum - length
try arraylist.reserve(&bytes, length + growth)
}
storage :: bytes.items.ptr[..bytes.capacity]
end := bytes.capacity
if (end > maximum) end = maximum
count :: try read(input, storage[length..end])
if (count == 0) return bytes
bytes.items = storage[..length + count]
}
}
write proc(output Writer, bytes []u8) usize ! WriteError {
if (bytes.len == 0) return 0
@@ -234,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,
@@ -263,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
@@ -279,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,
@@ -298,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,
@@ -333,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
}
@@ -392,7 +417,7 @@ write_float proc($T type, output Writer, value T, scientific bool) void ! WriteE
match typeinfo!(T) {
.float: {
buffer [64]mut u8 := undefined
count := 0
count c_int := 0
if sizeof!(T) == 4 {
if (scientific) count = c.snprintf(ptrcast!((&buffer).ptr), c_ulong(buffer.len), "%.8e", value)
else count = c.snprintf(ptrcast!((&buffer).ptr), c_ulong(buffer.len), "%.9g", value)
@@ -431,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[..])
}
+127
View File
@@ -0,0 +1,127 @@
import "@std/mem"
import "@std/arraylist"
import "@std/testing"
@hide:file
Input :: struct {
bytes []u8
offset usize = 0
chunk usize = 137
fail_at usize = maxval!(usize)
remaining usize = maxval!(usize)
live usize = 0
}
@hide:file
input_state proc(context ?@mut anyopaque) @mut Input {
if (context) |pointer| return ptrcast!(pointer)
unreachable
}
@hide:file
input_read proc(context ?@mut anyopaque, _ Handle, buffer []mut u8) usize ! ReadError {
state :: input_state(context)
if (state.offset >= state.fail_at) return .read_failed
count := state.bytes.len - state.offset
if (count > buffer.len) count = buffer.len
if (count > state.chunk) count = state.chunk
memcopy!(buffer[..count], state.bytes[state.offset..state.offset + count])
state.offset += count
return count
}
@hide:file
input_reader proc(state @mut Input) Reader {
return Reader{ context = state, handle = Handle{ file_desc = -1 }, read = input_read }
}
@hide:file
tracked_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
state :: input_state(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
tracked_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
state :: input_state(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
tracked_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
state :: input_state(context)
if (memory) |_| state.live -= 1
mem.raw_free(mem.c_allocator, memory, size, alignment)
}
@hide:file
tracked_vtable :: mem.AllocatorVTable{ alloc = tracked_alloc, realloc = tracked_realloc, free = tracked_free }
read_all_preserves_short_reads_across_buffer_growth test {
source [9000]mut u8 := undefined
for (0..source.len) |i| source[i] = u8(rem!(i, 251))
state := Input{ bytes = source[..] }
bytes := try read_all(mem.c_allocator, input_reader(&state), maxval!(usize))
defer arraylist.deinit(&bytes)
try testing.expect_equal(source[..], bytes.items)
}
read_all_accepts_exact_limits_and_probes_for_excess test {
for (usize(0)..4) |maximum| {
state := Input{ bytes = "abc"[..maximum], chunk = 1 }
bytes := try read_all(mem.c_allocator, input_reader(&state), maximum)
defer arraylist.deinit(&bytes)
try testing.expect_equal(state.bytes, bytes.items)
excess := Input{ bytes = "abcd", chunk = 1 }
unexpected := read_all(mem.c_allocator, input_reader(&excess), maximum) catch |err| {
try testing.expect(err == .limit_exceeded)
try testing.expect_equal(maximum + 1, excess.offset)
continue
}
arraylist.deinit(&unexpected)
try testing.expect(false)
}
}
read_all_releases_partial_storage_on_read_failure_and_limit_excess test {
for (0..2) |failure| {
state := Input{ bytes = "abcdef", chunk = 2 }
if (failure == 0) state.fail_at = 2
allocator :: mem.Allocator{ context = &state, vtable = &tracked_vtable }
unexpected := read_all(allocator, input_reader(&state), 4) catch |err| {
expected ReadAllError :: if (failure == 0) .read_failed else .limit_exceeded
try testing.expect_equal(expected, err)
try testing.expect_equal(0, state.live)
continue
}
arraylist.deinit(&unexpected)
try testing.expect(false)
}
}
read_all_releases_storage_at_each_failed_growth test {
source [9000]mut u8 := undefined
mem.set(source[..], u8('x'))
for (usize(0)..3) |remaining| {
state := Input{ bytes = source[..], remaining = remaining }
allocator :: mem.Allocator{ context = &state, vtable = &tracked_vtable }
unexpected := read_all(allocator, input_reader(&state), source.len) catch |err| {
try testing.expect(err == .out_of_memory)
try testing.expect_equal(0, state.live)
continue
}
arraylist.deinit(&unexpected)
try testing.expect(false)
}
}
+11 -19
View File
@@ -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| {
+63
View File
@@ -1,5 +1,68 @@
import "@std/io"
import "@ffi/c"
import "@std/mem"
Init :: struct {
io io.Io
args Args
}
#! borrows argument slices and their zero-terminated strings, valid until main returns.
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.
}
}
+26
View File
@@ -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
}
+67
View File
@@ -0,0 +1,67 @@
import "@ffi/c"
import "@std/mem"
ParseError :: enum {
invalid_number
out_of_range
}
#! parse a complete ASCII decimal with an optional sign, decimal point, and exponent.
#! at least one mantissa digit and, after an exponent marker, one exponent digit are required.
#! whitespace, embedded NUL bytes, trailing bytes, hexadecimal, infinities, and NaNs are invalid.
#! finite subnormals and signed zero succeed; overflow and nonzero values rounded to zero are out of range.
#! temporary storage uses `allocator` and is released before returning; the input is only borrowed.
#! the Darwin backend uses strtod_l with LC_C_LOCALE, independent of the process and thread locales.
#! decimal rounding follows native libc and the caller's floating-point rounding mode.
parse_float proc(allocator mem.Allocator, text []u8) f64 ! (mem.AllocError | ParseError) {
if (text.len == 0) return .invalid_number
cursor := usize(0)
if (text[cursor] == '+' or text[cursor] == '-') cursor += 1
digits := usize(0)
nonzero := false
point := false
while cursor < text.len {
byte :: text[cursor]
if (byte >= '0' and byte <= '9') {
digits += 1
if (byte != '0') nonzero = true
} else if (byte == '.' and !point) {
point = true
} else break
cursor += 1
}
if (digits == 0) return .invalid_number
if (cursor < text.len and (text[cursor] == 'e' or text[cursor] == 'E')) {
cursor += 1
if (cursor < text.len and (text[cursor] == '+' or text[cursor] == '-')) cursor += 1
start :: cursor
while (cursor < text.len and text[cursor] >= '0' and text[cursor] <= '9') cursor += 1
if (cursor == start) return .invalid_number
}
if (cursor != text.len) return .invalid_number
if (text.len == maxval!(usize)) return .out_of_memory
buffer :: try mem.alloc(u8, allocator, text.len + 1)
defer mem.free(allocator, buffer)
memcopy!(buffer[..text.len], text)
buffer[text.len] = 0
c.__error()?^ = 0
value :: f64(strtod_l(ptrcast!(buffer.ptr), null, null))
range_error :: c.__error()?^ == c.ERANGE
# binary64 magnitudes preserve subnormals despite libc's ERANGE; infinity starts at 9218868437227405312.
# ERANGE at magnitude >= 1 also catches overflow saturated by directed rounding.
magnitude :: ptrcast!(@u64, &value)^ & u64(9223372036854775807)
if (
magnitude >= u64(9218868437227405312)
or (magnitude == 0 and nonzero)
or (range_error and magnitude >= u64(4607182418800017408))
) return .out_of_range
return value
}
# Darwin's locale_t is an opaque pointer and LC_C_LOCALE is null (<_locale_posix2008.h>).
@hide:file
strtod_l c_func(text ?*c_char, end ?*mut ?*mut c_char, locale ?*mut anyopaque) c_double
+150
View File
@@ -0,0 +1,150 @@
import "@std/mem"
import "@std/testing"
@hide:file
expect_bits proc(text []u8, expected u64) void ! testing.Error {
value :: parse_float(mem.c_allocator, text) catch |_| { return .expectation_failed }
try testing.expect_equal(expected, ptrcast!(@u64, &value)^)
}
accepts_signed_decimal_forms_and_exponents test {
texts [7][]u8 :: ["1", "+1", "01", "1.", ".1e1", "10e-1", "1E+0"]
for (texts) |text| try testing.expect_equal(f64(1), try parse_float(mem.c_allocator, text))
try testing.expect_equal(f64(-1.25), try parse_float(mem.c_allocator, "-1.25"))
try testing.expect_equal(f64(0.5), try parse_float(mem.c_allocator, "+.5"))
try testing.expect_equal(f64(125), try parse_float(mem.c_allocator, "1.25e+2"))
try testing.expect_equal(f64(-0.125), try parse_float(mem.c_allocator, "-125E-3"))
}
accepts_borrowed_slices_without_a_terminator test {
bytes [5]mut u8 := ['x', '1', '.', '5', 'x']
try testing.expect_equal(f64(1.5), try parse_float(mem.c_allocator, bytes[1..4]))
try testing.expect_equal(u8('x'), bytes[0])
try testing.expect_equal(u8('x'), bytes[4])
}
rejects_malformed_or_partially_consumed_input test {
texts [33][]u8 :: [
"", "+", "-", ".", "+.", "-.", "e1", ".e1",
"1e", "1e+", "1e-", "1ee2", "1e1.0", "1.2.3", "--1", "+-1",
" 1", "1 ", "\t1", "1\n", "1x", "1_0", "1,5", "1\x00", "1\x002",
"0x1", "0x1p0", "inf", "-Infinity", "nan", "NaN(1)", "\xff", "1\xff",
]
for texts |text| {
_ = parse_float(mem.c_allocator, text) catch |error| {
try testing.expect(error == .invalid_number)
continue
}
try testing.expect(false)
}
}
preserves_signed_zero_even_with_extreme_exponents test {
positive [4][]u8 :: ["0", "+0.0", "0e999999", "0e-999999"]
negative [4][]u8 :: ["-0", "-0.0", "-0e999999", "-0e-999999"]
for positive |text| {
try expect_bits(text, u64(0))
}
for negative |text| {
try expect_bits(text, maxval!(u64) - u64(9223372036854775807))
}
}
rounds_decimal_midpoints_to_nearest_even test {
# IEEE-754 binary64 encodings: 0.1, 1, and the next two values above 1.
try expect_bits("0.1", u64(4591870180066957722))
try expect_bits("1.00000000000000011102230246251565404236316680908203125", u64(4607182418800017408))
try expect_bits("1.00000000000000011102230246251565404236316680908203126", u64(4607182418800017409))
try expect_bits("1.00000000000000033306690738754696212708950042724609375", u64(4607182418800017410))
try expect_bits("9007199254740993", u64(4845873199050653696))
}
accepts_finite_representational_boundaries_and_subnormals test {
# smallest subnormal, largest subnormal, smallest normal, and largest finite binary64.
try expect_bits("4.9406564584124654e-324", u64(1))
try expect_bits("2.4703282292062328e-324", u64(1))
try expect_bits("-4.9406564584124654e-324", maxval!(u64) - u64(9223372036854775806))
try expect_bits("2.225073858507201e-308", u64(4503599627370495))
try expect_bits("2.2250738585072014e-308", u64(4503599627370496))
try expect_bits("1.7976931348623157e308", u64(9218868437227405311))
try expect_bits("1.7976931348623158e308", u64(9218868437227405311))
try expect_bits("-1.7976931348623157e308", maxval!(u64) - u64(4503599627370496))
}
rejects_overflow_and_nonzero_underflow_to_zero test {
texts [12][]u8 :: [
"1e309", "-1e309", "1.7976931348623159e308", "-1.7976931348623159e308",
"1e999999", "-1e999999", "1e-999999", "-1e-999999", "1e-324", "-1e-324",
"2.4703282292062327e-324", "-2.4703282292062327e-324",
]
for texts |text| {
_ = parse_float(mem.c_allocator, text) catch |error| {
try testing.expect(error == .out_of_range)
continue
}
try testing.expect(false)
}
}
@hide:file
AllocationState :: struct {
remaining usize = maxval!(usize)
live usize = 0
}
@hide:file
tracked_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
tracked_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 (new_size == 0) {
if (memory) |_| state.live -= 1
} else if (resized) |_| {
if (memory) |_| {} else state.live += 1
}
return resized
}
@hide:file
tracked_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
tracked_vtable :: mem.AllocatorVTable{ alloc = tracked_alloc, realloc = tracked_realloc, free = tracked_free }
releases_temporary_storage_and_propagates_allocation_failure test {
state := AllocationState{}
allocator :: mem.Allocator{ context = &state, vtable = &tracked_vtable }
try testing.expect_equal(f64(1.5), try parse_float(allocator, "1.5"))
try testing.expect_equal(usize(0), state.live)
texts [2][]u8 :: ["1e999999", "1e-999999"]
for texts |text| {
_ = parse_float(allocator, text) catch |error| {
try testing.expect(error == .out_of_range)
try testing.expect_equal(usize(0), state.live)
continue
}
try testing.expect(false)
}
state.remaining = 0
_ = parse_float(allocator, "1.5") catch |error| {
try testing.expect(error == .out_of_memory)
try testing.expect_equal(usize(0), state.live)
return
}
try testing.expect(false)
}
+4 -8
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff