Since 0.0.8, every change package raises the patch number by one, and a
heading below is one package: what it decided, what it changed, what it left
open. The version is the specification’s; the compiler’s crates carry their own.
A change package is a change to the language - the compiler, the parser, std
or the specification. A change only to the guide, the notes, the README, the
website, the tests, the examples or the tooling around them raises nothing and
has no heading here; CLAUDE.md draws the line.
immutable: the reserved word, the binding inside a function and the deep rule NK1331 (ADR-389 D1, D2, D6)
check: a lent match binding is the pattern of a text method as it is, with no & of its own (check_host a_lent_part_is_a_reference_already) (#1176:, #1178); check: a copy of a tuple with view parts is a tuple of its own, each view part copied (ADR-293 D24) (#1184); check: a number type is not a std module, so u64::from no longer asks for use std::u64 (#1186); checker: a text item a walk lends looks up a text-keyed map as str (#1190, #1190); checker: a let of a field of a call’s owned result moves it out (#1191); checker: an element read past a jump of a struct declared beside is a view (#1183); checker: a let of a field of a call’s owned result moves it out (#1191); describe: crate_root (describe.rs into describe.nika) (#1193); lowering: text into a fs::Path parameter the callee only reads is lent as a PathBuf (#1195); lowering: m.get(k)?.field lends the member like m[k]?.field (#1194); lowering: text into a fs::Path parameter the callee only reads is lent as a PathBuf (#1195); std: String::pop and String::insert_str; OutText’s take_lend and brace inserts move into emit_out.nika (#1180); ; specbook: which reading got furthest, and the report, in Nikaia; contracts: infer_package_noted’s pass order, the keeps fixpoint, evaluate_defaults and expression_ensures move into ledger_items.nika (contracts/mod.rs, Part: #125)
match arm only reads, bound from an owned option, goes to strip_suffix, ends_with, split and the other text-pattern methods without a second & (#1178); a binding handed on through a ?? with nothing to replace is moved (#1176).found.clone() of a (String, String) read from a map of literal text now hands back owned text instead of being refused with NK1104 (#1184, ADR-293 D24 for tuples).scalar::is_ascii_hexdigit. Fixed: a return inside an expression in a function handing back a view or owned text compiles. Fixed: u64::from(b) no longer asks for use std::u64.get/first/last of a Vec[ref String] copies the &str out; #1190 a filter after a map looks a text-keyed map or set up through as_text, no &&Stringlet x = call().field moves the field out of the call’s owned result instead of borrowing a temporary; a field of a call that hands back a ref stays a view (#1191).let x = call().field moves the field out of the value the call hands over instead of borrowing a temporary; a field of a call that hands back a ref stays a view (#1191).fs::Path.parts() (ADR-319 D10), and == on paths compares whole parts as D9 says; describe.rs’s without_dots, crate_sources and crate_root now in tools/describe.nika?. after get/first/last off a binding lends the member instead of moving it (#1194); text handed to an owned fs::Path parameter the function only reads is lent as &PathBuf::from(..) (ADR-319 D2, #1195)?. after a get, first or last off a binding lends the member out of what the read finds instead of moving it (E0507)fs::Path parameter the function only reads is lent as &PathBuf::from(..) (ADR-319 D2), not passed as &strget past ?? continue (or a match arm on it) no longer binds its numbers as references (#1181); text_tiers: .to_string() of an owned or number local stays owned (ADR-282 D8) (#1005); std: every set has iter() (ADR-collections-2 D3) (#1006); check, emit: sum() names its result type (#1007); ; ; ; check: a lambda typed -> T? wraps its non-null arms in Some (#1017); ; checker: a let that shadows a fn parameter is not the lent parameter (Fixes #1023); check: a fn parameter lent to a kept position is refused with NK2101 (#1024); keeps: a parameter a kept lambda names is kept (#1025); ; ; ; keeps: a lambda handed out keeps the fn parameters it names (#1034); ; buffers: a tuple result with a view part keeps the argument made in the call (Fixes #1036); ; ; emit: an untyped let lambda run by a parameter that may pause is an async closure (#1046); ; ; ; check: a map’s read as the fallback of ?? beside an owned T? is copied or refused (ADR-293 D15) (#995); ; ; contracts: the stored-code fold moves into ledger_items.nika; the dead LedgerOps::candidates goes (C1b); contracts: keeps’ inference moves into keeps.nika (K4); ; type_rules: chunk 2 (expand, literals, given) moves into type_rules.nika (R-3); Parts taken out of a mut parameter, and a narrowed mut option, change in place; A part handed to a mut parameter is lent, not given away; Remove the architect role: the supervisor writes the briefs, the driver stops for it; sysroot: the tools package is assembled in sysroot_package.nika (S1); std: Entry::or_default and Seq::cloned (Fixes #1082); std: process::run takes stdin, stdout and stderr options (Fixes #920); ; std: fs::Metadata::modified, links, file_id and time::since_epoch
.to_string() of a local holding text of its own or a number is owned text again, so a -> String function may return it (ADR-282 D8, #1005).std gains iter() on collections::BTreeSet (smallest first) and collections::HashSet (no order), lending the elements one at a time.keeps column runs; NK1265 warns when a pub trait method’s column changes, naming the impl (ADR-277 D17, #1012).Some, as a function does (#1017).String parameter captured by a lambda a callee keeps and used afterwards is refused with NK2101, no longer by rustc (#1024).let or a field that calls a function value it captures now shares that value’s closure. Before, rustc refused the lowering.fn parameters (returned, assigned, or put in a struct, list or tuple) keeps that parameter, instead of a lowering rustc refused.return h.f over h: ref H) is a copy that shares its closure, no longer refused NK1131.let and handed where a function value is kept is kept, instead of a lowering rustc refused.let and put into a function field no longer makes a call through the field refused NK2605.let and handed to a function parameter that may pause now lowers as an async closure instead of being refused by rustc.fn schema[T](T) takes a type where the parameter list holds one, schema(User), and a call fills it from a type, a type parameter, field.ty or an arm’s binding; a value there is NK1230 (ADR-330 D1); and pick[User](1) fills a function’s type parameters in order; warning[NK1233] (naming #514) only where a value argument already says the type, id[i64](3) (ADR-330 D9, ADR-grammar-2 D11)[[1, 2], [3]]) has one open number for its innermost elements: its uses through an index or a walk type it, and the program builds where it fell back to i32 in rustc or was refused with NK1245 (ADR-285 D34, #938).seq.?? beside an owned T? is copied out where the value copies and refused with ?.clone() otherwise, instead of reaching rustc (#995).String? (return m.get(k), return m[k], or through a let) copies the read out instead of reaching rustc (#996).let without types takes its parameter types from the first call it is handed to, and a map read as its last value is copied out.tools/keeps.nika, behind a KeepsHost.mut parameter, and a narrowed mut option, can be handed on mut and changed in place.for mut element handed to a mut parameter is lent to it, not given away, wherever its type is declared.std: m.entry(k).or_default() puts in the value V() makes (a collection, String, a number, bool, a struct of defaults; another V is NK1102), and xs.iter().cloned() hands on a copy of each element (ADR-384).hash::sha256_hex(data) is the SHA-256 digest in lowercase hex; env::var(name) reads an environment variable (null when unset, empty or not Unicode) and env::temp_dir() names the temporary directory (#922 steps 1-2)fs::Metadata::modified(), links() and file_id(), and time::since_epoch(), the wall clock to compare a file’s time with (#922, ADR-319, ADR-368)a taking arm over an enum of another file; a number bound under a match on a part held in place (#125)
match arm over an enum another file declares leaves that enum’s placeholder (it lowered to a type mismatch).match over a part held in place out of a mut parameter is copied out (it lowered to a comparison of &mut i64 with i64).match over a place inside a mut parameter whose arm changes a part in place moves the part out; a taking arm that gives the value back on only one branch is accepted.a variant part of code keeps its lambda; a text field asked of a list of text compares as text (#125)
St::Ex("a", fn(x) { x + 1 }) lowered to a bare closure where kept code was expected).contains on a list, array or slice of text compares as text (it lowered to a mismatch of &String and &'static str).mut; a typed lambda parameter (fn(k: ref String) -> bool { … }) does not parse.a part handed to a mut parameter is lent; parts a taking arm takes out of a mut parameter change in place (#125)
for mut element handed to a mut parameter is lent, not given away (it was refused with NK1131).mut parameter, and a narrowed mut option, are changed in place.rs2nika: a type the cut does not declare has its methods freed too; rs2nika: a text cut at a number goes through its bytes (ADR-374); rs2nika: let with every name mut is let mut (..)
t.bytes[a..<b].text(), get(a..), starts_with on bytes, unwrap_or_default -> ?? ""), ADR-374.A function type may take a function parameter (#1120)
Optional code narrows to a callable function; a match arm keeps a moved part of a by-value parameter (#1117, #1092, #125)
self-hosting: project.rs’s ledger and proofs writing moves into project_write.nika; rs2nika R334 (#125)
self-hosting: a project’s drive and drive_to move into project_drive.nika (#125)
self-hosting: the prover’s run moves into prove_run.nika (#125)
prove.rs (535 to 326 lines) is prove_run.nika; model_of stays in Rust until #1092.self-hosting: proofs.rs’s answers move into proofs_answers.nika (#125)
proofs_answers.nika; proofs.rs keeps the thread-local state and the scope combinators.check: a list or map element past a jump compares with a value (#1112)
m.get(k) ?? return (named or inline) compared with == against a value that does not copy is lowered as a view, so rustc no longer refuses it (#1112).spec: ref in front of a tuple (#1115); a path orders, equates and hashes (ADR-319 D9, #1116)
ref may stand in front of a tuple type, as in front of a type name; a tuple whose parts all copy is refused with NK1257 (#1115, to build).fs::Path equates, orders part by part and hashes (#1116, to build).Comptime hands a turn’s field to a function; a sync call with type arguments in brackets (#1087, #1095)
T? now crosses from build time, null included (#1087).sync function or a comptime may call a generic function with type arguments in brackets, id[i64](3); it takes that function’s promise (#1095).prover: loop turns and collection lengths bound counters, argument lengths, every length below 2^63, sound preconditions; claims in the figure (#1053, #1054, #1069, #1076, #1084, #1090)
assert inside a loop body that reads a name the loop changes is proved through that loop’s invariant at the head of the turn, with the loop condition and the path from the head (ADR-361 D2, D3); before, only a claim after the loop was.nikaia test compares a test program’s --bounds report with tests/NAME.bounds (at prove-through-loops:off) and tests/NAME.loops.bounds (at :on) where they exist, and --bless writes them, so a proof is guarded by a test.for over a range or a while with a step counter grows by constants is bounded by those turns, so its overflow check is proved.for over a list, map or set as one turn per element, so a counter stepped once a turn in such a loop is proved within the collection’s length and loses its overflow check.--asserts lists it as not proved, and the default build refuses it with NK1300 (#1076).nikaia lower-std --asserts (with or without --bounds) prints each tools file’s --asserts report, so scripts/proved.nika can count the tools package’s claims beside its indexes and operations.Bytes(x) (x’s), of a bound [v; n] (let as well as let mut), and of a list literal or repeat passed straight to a function, so a length precondition these plainly meet is proved (#1084).i < xs.len() proves i + 1 and a for’s counter is proved for every element type, and a list of a struct another file declares is bounded below 2^60 as one of the file’s own (ADR-385 D1, #1053).Grammars build a struct from keyed members; a function takes a reflected field (#1019)
fn f(field: meta::Field): each call in an unrolled turn calls a copy made for that field, the field is not moved, and meta::Field anywhere but as a parameter’s type is NK1320.members[S](key, value, sep; null:, unknown:) in a grammar builds the struct S from keyed members by the mapping rule of ADR-grammar-2 D3, D5: a missing field takes its default or null, a member given twice or an unknown member fails, with a message naming the field or key.T? crosses from build time wherever T does (null is a constant), decided for #1087; its build follows.Grammar rules choose a pattern by a type’s kind
match T::kind { … }, refuses a type with refuse("why"), and checks its type parameters’ bounds at the call.views: a by-value parameter that holds no text is no view source (#1070); views: a list of views handed back may be a literal or grown with extend (#1071); tiers: a view field read off a match payload of a lent struct is a view (#1072)
extend, as one built by push already could.String field that holds a view can be copied from a struct bound by a match pattern (a tuple or named variant payload, or an element of a payload list) of a borrowed value into a new struct.Grammar rules take type parameters
rule Value[T] -> T, called G::read[i64](input) and Value[T] in a pattern; one copy per list of types, a value in the brackets is NK1230 (ADR-grammar-2 D1, #1019).nikaia test on parallel workers; loop checks through the prover’s candidates; –bounds names its reasons (#1053, #1054)
nikaia test builds each output test once against a kept package directory, runs the built program directly, and runs the tests on parallel workers (NIKAIA_TEST_JOBS, default the core count), with the same output in the same order; a package’s ledger, proofs and lockfile are written whole, so builds side by side never read them half-written.--bounds names why a check stayed from how the name was bound: a counter a loop changes, an if/match value, a parameter, a value read out of a list, or another list’s length (#1053).while loop’s open checks are asked with the prover’s loop candidates (closed forms, intervals, guess and check) and the values the loop is reached with (ADR-361 D10, #1054 step 2); an assert inside a loop body is proved through that loop’s invariant (ADR-361 D2, D3).nikaia lower-std --bounds prints the --bounds report of each tools file, checked with the package beside it (#1053).check, emit: type arguments at a call reached by a path (ADR-grammar-2 D11)
path::f[T](…) fills a function of another package or std the same way f[T](…) does, warning[NK1233] only where a value argument already says the type (ADR-330 D9, ADR-grammar-2 D11). A path callee with more than one type parameter is still read as an index: a read ledger signature keeps no unbounded type parameter.spec: NK1233’s example is one a value already says (ADR-grammar-2 D11, ADR-330 D9)
id[i64](3): parse[Config](text), shown as warned in ADR-330 D9, ADR-grammar-2 D11 and Part III, is not warned under D11’s rule (no value argument says T), as built at 0.0.1319. The rule stands; the examples are corrected (owner, 2026-10-31).parser, check, emit: a type as an argument, type arguments at a call (ADR-330 D1, D9, ADR-grammar-2 D11, #859 steps 2, 8)
fn schema[T](T) takes a type where the parameter list holds one, schema(User); a call fills it from a named type, a type parameter, field.ty or a match arm’s binding, recursion included. A value there, or a type where a value belongs, is NK1230 (ADR-330 D1).pick[User](1) fills a function’s type parameters in order, for a function the file declares; warning[NK1233], naming #514, only where a value argument already says the type, id[i64](3) (ADR-330 D9, ADR-grammar-2 D11). A method’s x.f[T]() stays refused.Grammar: hex_digit is one hexadecimal digit (#1002)
hex_digit, listed in Part II 10.8, reached the engine as Undefined rule. winnow-grammar now has it as it has digit: one scalar of 0-9a-fA-F, hex_digit{2} a fixed-width run on the same fast path, hex_digit+ a run as text, expected as “a hex digit”. The pinned rev moves to 4f8e6691.check, emit: type kinds, a reflected field’s .ty, match on a kind (ADR-330 D2-D6, #859 steps 3, 1, 4)
T::kind and T::name exist: a generic function that asks them is written once per type, T::kind == meta::Type::I64 is decided while building, and a meta::Type that would reach the running program is NK1231.T::fields, field.ty::name, field.ty::kind and field.ty::fields name the field’s type, and expr.name::member parses (ADR-330 D5).match T::kind { Text => ..., List(it) => ..., Struct(s) => ... } checks every arm under its kind and builds the arm each type takes (ADR-330 D4).Kept callbacks: a fn parameter lent to a kept position is refused with NK2101 (#1024), a parameter a kept lambda names is kept (#1025), NK1264/NK1265 (#1012); grammar labels on literals and one-element groups bind (#1027); a call in a guard is a call site (#1032)
&dyn Fn, and a let that shadows a fn parameter is no longer taken for the lent parameter (#1023); a kept callback that crosses threads names std::marker::Send + Sync.NK2101 (#1024); a parameter that a kept lambda names is kept itself (#1025).NK1264 refuses a keeper in another package’s impl of a trait whose column lends; NK1265 warns when a trait’s column changes (ADR-277 D17, #1012).-> T? wraps its non-null arms in Some (#1017); a kept lambda’s integer result types its literals (#1029); a block arm’s lent optional part opens its option (#1029); a kept fn read by index shares the closure (#1018).The prover follows short loops turn by turn and a match as an if (ADR-361 D9, D12; #639, #1009, #1021); NK1241 refuses a wide integer as scalar (#1003); a callback through a trait borrows (ADR-277 D17 step 1, #1012); the NK2104 note no longer names seq (#994)
n = n * 16 + d over four turns proves n <= 0xFFFF; a throw in a loop body is a way out of the loop, and a whole-number let the prover does not read is a name of its own (ADR-361 D12, #639).let mut bound to a call, a whole number handed to a function that does not change it, what a branch’s own let of a call bounds past the if, and a call’s precondition through the history of an if, so json’s surrogate-pair decoding proves scalar::from (#1009).let mut whole number through a match statement as through an if: each arm knows what its bool, literal or range pattern says of the matched value, and after the match the number is one arm’s value or another’s (ADR-361 D9, #1021).u8 cast as scalar is refused with NK1241, whose help names scalar::from(n), instead of being checked at run time (#1003).match over a tuple of views binds the parts inside each view by reference and copies numbers and bools out.NK2104 note for stdout against stderr no longer sends the reader to the removed seq (#994).ADR-grammar-2 accepted: a grammar entry takes a type and is emitted per type; members[S] builds the struct; json::read forwards to the grammar (#604)
Json::read[Contact](bytes)); the grammar is emitted once per list of type arguments, and a match on T::kind in a rule body chooses the pattern, only the arm the type takes emitted (Part II 10.8, ADR-grammar-2 D1, D2).members[S](key, value, sep; name:, null:, unknown:) builds a struct from named members; the compiler matches keys to fields and builds the value by one mapping rule, shared with SQL’s dsl … -> T (ADR-299 D16-D18), so no construction from reflected fields is needed (D3-D5, D9).json::read[T] forwards to the package grammar’s entry Json::read[T]; parse is its second entry; write[T] stays a function over T::fields (ADR-344 D3, D6a changed; D8, D10).NK1233 is narrowed: no warning where the brackets alone name the type (ADR-330 D9 changed; ADR-grammar-2 D11). NK1250 covers a type no arm of a grammar’s kind match takes, or that an arm refuses, a type with a text form of its own and an enum variant with a payload (D6, D12, D13); an enum variant without payload is written as its name.NK1310 a match in a pattern on a value not known while building, NK1311 two fields with one name in the input, NK1312 a field name: leaves unread with no default and not a T?.json package also on a grammar exported by a package (#520).ADR-277 D17: a trait method’s keeps column (#1012); sum() names its type (#1007); a returned map read of literal text is copied (#996); a binding inside a bound text(..) is kept (#1013)
pub trait method’s keeps warns (NK1265).sum() names its result type, so a sum of a typed sequence compiles where the type is not otherwise written (#1007).text(..) of the grammar is kept (#1013 point 1).an impl’s method keeps what its trait method keeps (#1008)
sets are walked by iter(); a kept function value is handed on more than once (#1006, #1008)
std gains iter() on collections::BTreeSet (smallest first) and collections::HashSet (no order), lending the elements one at a time (ADR-collections-2 D3, #1006).text tiers: .to_string() of an owned or number local stays owned (#1005)
.to_string() of a local holding text of its own or a number is owned text again, so a -> String function may return it (ADR-282 D8, #1005).check, emit: a let-bound lambda takes its parameter types from its first use (#997); text tiers: to_string() of a number, bool or char is new text, also of a part a pattern binds (#998)
let without types takes its parameter types from the first call it is handed to, and a map read as its last value is copied out..to_string() of a number, bool or char, also one a match arm binds, now hands back text of its own instead of a view that NK2302 or NK1104 refused.The concurrency design is closed (ADR-async-2 D9, ADR-async-3 D7, ADR-async-4 D13-D15, #983). async and sync stay, and the option names handle, deferred, detached and on_timeout are final. within: on async(...) is a deadline from the start on every form; the task::TimedOut it gives is a failure and meets the use-site catch, then the fallback, then the receiver. h.join(within: d) limits that wait: when it runs out, task::TimedOut belongs to the join, the task keeps running and the handle stays live (NK2109 asks for it to be taken on that path too); on_timeout: task::OnTimeout::Cancel cancels it instead (changes ADR-346 D3, D4). The lambdas of fold, par_pieces and par_fold never pause (NK2209, whose help names map then fold). Part I 8.1 is now “Pausing Without Marking Functions”, and Part II 11.1 “Implicit Pausing and the Scheduler”. Nothing of it is built yet (#984-#991).
**fmt: a closure’s tuple pattern is not cut (fix-forward, CI red since 0f9a525); lowering: text a method hands back as its own is this frame’s buffer, and reaches no parameter (#933); check: NK2605 only for a failing cleanup still held where its block ends (#934); text tiers: a viewed text moved into a literal is kept, or refused at the move where the field owns (#942); text tiers: a literal bound by a let is declared String, never a String? taken from the program (#943); text tiers: a struct result viewing a local takes the caller’s keep; project_lower_published takes the ledger’s pairs out of the entry instead of copying them (ADR-views-1) (#949); a part taken out of a let mut view or a mut parameter is given back before its block is left (ADR-views-1); ; check_core_call_on: the two receiver/argument binding merges use BTreeMap::entry; check_program, fold_bound, throws: contains_key-then-insert sites with the key given away use BTreeMap::entry; rs2nika: for x in xs.iter_mut() / &mut xs is drafted as for mut x in xs (#951 step 4); let mut x = m.get(k) ??
fn((a, _))) when it breaks a long line.return host.hex(seed).trim() through a local), now lives in the caller’s keep instead of lowering as a view of the parameters that rustc refused.close() on every path of its block, is no longer refused with NK2605.let is no longer typed String? when the program writes a String? anywhere (#943).let mut view of a place (let mut c = m[k] ?? continue, a for mut element) or of a mut parameter handed to something that keeps it is taken out with no copy (std::mem::take, or mem::replace with a stand-in) and must be given back by assignment before the view’s block is left (NK2108, naming the view and its place); the whole view handed over is NK1131 (ADR-views-1, #949)let mut view of a place (let mut c = m[k] ?? continue, a for mut element) or of a mut parameter handed to something that keeps it is taken out with no copy (std::mem::take, or mem::replace with a stand-in) and must be given back by assignment before the view’s block is left (NK2108, naming the view and its place); the whole view handed over is NK1131 (ADR-views-1, #949)BTreeMap::entry, HashSet::extend and BTreeSet::extend exist; Vec::extend takes a list, a set or a sequence; s.extend(xs.clone()) copies the elements, not the list (ADR-collections-1, #950).let mut x = m.get(k) ?? <jump> over a mut map or list is a mutable view of the entry, as m[k] is (get_mut), instead of Rust code rustc refused (#952).match arm name bound by reference over a value (an option field of an .iter() loop variable) compares with a view or a value as it should (#954)String part of a tuple looked up in a container that holds text of its own (set.remove((1, other))), or kept in a published String field of a struct another file of the package declares, is declared String (ADR-282 D17, #956)extend on a list, HashSet or BTreeSet refuses elements of the wrong type with NK1102 instead of a rustc error (#963).push, insert, contains, contains_key, remove, get and entry on lists, sets and maps refuse an element, key or value of the wrong plain type with NK1102 instead of a rustc error (#964).push, insert, contains, get and the like on lists, sets and maps compare tuples, lists and options structurally and refuse a wrong one with NK1102; a text view, a character for a byte and a declared type for its owner are accepted again (#965).push or insert through an if or match arm beside a literal arm (ps.push(if w == "()" { "_" } else { w })), is moved as its own instead of being kept as a view, which rustc refused (#969)extend of a list literal of text into a Vec[String], HashSet[String] or BTreeSet[String] builds owned text again (rustc E0271 since 0.0.1286); extend of text literals into a list or set of text views is no longer refused with a false NK1102 (since 0.0.1288)extend of a list literal of text into a Vec[String], a HashSet[String] or a BTreeSet[String] builds owned text again (rustc E0271 since 0.0.1286)extend of text literals into a list or set of text views is no longer refused with a false NK1102Manifest’s parse and readers are Nikaia (ManifestFile, manifest_parse); the toml crate stays behind the adapter (MF, #125)async is reserved, overlap leaves the reserved-word list (ADR-async-4). let async = 3 is refused as a reserved word, with async_ suggested; the grammar has no async expression yet. overlap { … } still parses through its own rule and stays refused as a name until it is removed (#991). The compiler’s list matches Part I’s again.
async is an expression prefix and the one way to start a task (ADR-async-2, -3, -4, #983). async e starts at once and yields the value, waited where it is first needed - a mention, the call that receives it as an argument, an operation - so f(async g(), async h()) runs both beside each other; it cannot be cancelled. async(handle) gives a group-bound handle with join and cancel, async(deferred) one that runs on start(), async(detached) a free handle that may outlive its group (it replaces spawn, which stays accepted until the build). Cancelling is .cancel() on a handle only; task::cancel is gone. A catch after an async expression is the last handler before the receiver, for every form. Every task is a child of a group with one capture, failure and memory rule. Fan-out over a collection is par_iter, whose lambdas may now pause, under the same rules (NK2104, kept failures, limit: replaces workers:); the async loop is removed, and async inside a loop body is NK2120. Codes NK2119-NK2122 added, NK2113, NK2114, NK2116, NK2117 retired. Option names are placeholders until the naming round.
The grammar engine is winnow-grammar 3b2dd3d: reading from a Read (its ADR 27)
parse_<rule>_reader for a par_fold rule (blocks cut past the last frame boundary, merged in order) and #[stream] for a fold reached through a static path, one item at a time (YAML’s documents, one long top-level sequence, a JSON array, an XML root’s children), with #[stream(each)] to see every item. Errors of a stream carry their line.#[stream] or call the reader entries; that comes with the yaml package (#928).async replaces overlap and task::scope (ADR-async-1, #983). The specification now writes structured concurrency with one keyword and no await. async let x = e starts a child that runs beside the code after it and is waited for before the first statement naming x; async let xs = for … { … } is a group whose value is the list in input order; async e, async fn { … } and async for start children without a value; async { … } is a group boundary. Options limit:, within: and admission:; without limit: an exchangeable admission strategy starts a child only where its memory block and a descriptor fit. Failures: a use-site catch first, then the binding’s fallback catch, else the error leaves from the wait point; every crossing error records started and arrived sites. task::cancel(x); a full memory block arrives as task::OverBudget at every boundary, and only a bug panic cancels siblings (changes ADR-327 D6); the leaving error leads (changes ADR-346 D7). New codes NK2110-NK2118. The compiler is built in #984-#991; until then it still takes overlap.
Checker defects (lane C). A text field of a borrowed subject passed to a parameter whose contract keeps nothing of it is lent, not refused NK1131; a parameter that keeps its value still refuses it (#936). A ref T? parameter proved present reads as ref T, and an option bound by reference out of a lent subject is put back into a ref T? slot as Option::as_ref(x) (#898). A tuple literal given to a kept generic parameter is checked part by part, and a lent part is refused NK1102 with the .clone() help (#909). The element, key or value argument of push, insert, contains, remove, get, contains_key and entry gives an untyped integer the receiver’s element type (#917). What sum or product hands back of a list of number literals takes that list’s type, i64 when nothing asks for another (#931).
Lowering defects (lane E). A ?? default of split_once/rsplit_once stays a pair of views when every default part is a literal or a place, otherwise both halves become owned Strings (#908). A trim* or trim_*_matches followed by .clone() is a copy, not a view (#915, #910). A String place assigned an either-text gets .into(); the runtime gains From<EitherText> for String and PartialEq<EitherText> for String (#911, part of #925). ?.clone() of a ref Bytes? gives owned bytes, and ref Bytes? is lent as Option<&[u8]> (#913). A match arm whose body is an if lowers as a block, a field read in a nested match subject is copied out, and an else after arms naming every variant of an enum the file declares is not written (ADR-357 D5) (#930, #940, #918). An f"…" argument reading a mut argument of the same call is hoisted (part of #899); its holes read before a ref mut self call are copied first, and a changed binding lowers as *x, parenthesised only as a postfix base (#970, #937).
Text tiers: an element reached by index, get or a for binding owns the text changed through it (#967, #981); NK2305 names the list’s place (#971). A text field of a list or map element changed in place through v[i], a let of the element or a for binding, and a text element of a list or map changed in place through a let of xs[i], m[k] or m.get(k) (also before ?? return), now makes that element text of its own instead of lowering a borrowed text the change cannot write to. NK2305’s help for a for mut loop whose list is read in the body names the list’s place (for i in 0..<self.pending.len()), not its root.
ADR-error-sets-1 (#625, specified, not built): a code parameter that the function’s body does not keep fails, at each call, only with what that call hands it (for private and pub functions alike), while a kept parameter keeps the union of everything stored in it (ADR-351 D8).
A let over a part of a mut parameter, ref mut self or a let mut view is a view while the name is only read, and takes the part out with no copy when the name is kept. Inside a loop, the part is given back before the turn ends. A refusal that comes from the taking names the line that keeps the name (ADR-views-1 D6, Part I 6.5, #979). Specified; not built yet.
A lambda handed to a parameter that runs it at most once may give away what it captures; a captured value it hands over is gone after the call (no NK2105 repeat inside such a lambda). std names these parameters with the contract key once (Entry::or_insert_with, Entry::and_modify, task::scope, Locked::access, SharedMut::access; not update). For user code, once-ness is inferred, lowered as impl FnOnce, and never written (ADR-lambdas-1, Part I 5.4 D, #973). Specified; not built yet.
An assignment or compound assignment to a let mut view (c = v, c += 1) writes through it, and an entry view of a number, bool or char reads as the value; before, it was NK1105 or rustc’s E0368 (#976). The text an or_insert_with lambda ends in is built as the map’s value type; before, rustc reported mismatched types (#977).
let mut x = m.entry(k).or_insert(v) (or or_insert_with) binds the &mut that Rust hands back, and lowers with no mut (#974). A let mut view (an entry’s or_insert, m[k] ?? jump, m.get(k) ?? jump, v[i]) passed to a mut parameter is lent on as &mut *x; before, it was NK1102 or rustc’s E0596 (#975).
A part of a let mut view of a place (let mut c = m[k] ?? continue, a for mut element) or of a mut parameter, handed to something that keeps it, is taken out with no copy (std::mem::take, or mem::replace with a stand-in) and must be given back by assignment before the view’s block is left (NK2108, naming the view and its place); the whole view handed over is NK1131 (ADR-views-1, #949).
Text a call returned, handed whole to push or insert through an if or match arm beside a literal arm (ps.push(if w == "()" { "_" } else { w })), is moved as its own instead of being kept as a view, which rustc refused (#969).
extend of a list literal of text into a Vec[String], HashSet[String] or BTreeSet[String] builds owned text again (rustc E0271 since 0.0.1286); extend of text literals into a list or set of text views is no longer refused with a false NK1102 (since 0.0.1288) (#950, #963).
Collection methods (push, insert, contains, get, …) check a composite argument part by part (tuples, lists, options) (#965); a text view fits String, a character fits a byte, and declared types fit each other again, which ends the false NK1102 of 0.0.1289 (#964).
for mut x in xs writes through to a list or an array held mut (a view of the element, lowered iter_mut()), and is a let mut of the turn over drain() or a range; mut before one name of a tuple is NK1307 (not yet supported); mut on a map key, a set element or an adapter’s element is NK1306; a read of the list in the body is NK2305 (#951, steps 1-3). extend of a comptime array of text into a String list or set compiles (rustc E0271, #966).
push, insert, contains, contains_key, remove, get and entry on lists, sets and maps refuse an element, key or value of the wrong plain type with NK1102 instead of a rustc error (Fixes #964).
str and String count as one. Lists, tuples, options and aliases as arguments are not compared yet.extend on a list, a HashSet or a BTreeSet refuses elements of the wrong type with NK1102 instead of a rustc error (ADR-collections-1, Fixes #963).
let mut e = v[i] and let mut f = s.field (a struct of numbers) are mutable views: reading the list or struct before the view’s last use is NK2305 instead of a rustc error, and a write through f reaches s (Part I 6.8, Fixes #962).
mut stands before the whole for head; one mut name inside a tuple (let (tx, mut rx), for (k, mut v) in m) is refused for now (NK1307, reserved), and map values written in place wait for it (#951).BTreeMap::entry, HashSet::extend and BTreeSet::extend exist; Vec::extend takes a list, a set or a sequence; s.extend(xs.clone()) copies the elements, not the list (ADR-collections-1, part of #950).
for mut x in v is a let mut view of each element, written back; keys and set elements stay unchangeable (NK1306) (ADR-views-2, #951).A name bound to a text literal and changed in place (push_str, +=) is declared String, and the set, list of tuples or typed let it goes into holds String, not &'static str (ADR-282 D17, Fixes #961).
Array[T, N] and a run ref Array[T] get iter() with the sequence surface of Vec::iter(): zip, map, filter, enumerate and rev follow (ADR-collections-2 D1, Fixes #955).
bind_code in tools/ty.nika zips the two parameter lists instead of walking an index.let mut e = v[i] over a mut list binds a view of the element (&mut v[i]), also for a struct of numbers: a write through e reaches the list instead of being lost in a copy (Fixes #959).
A for lends its list: changing it in the body (a mutating method, an element assignment, an assignment of the whole list) is NK2305 instead of a rustc error; reading stays allowed (Part I 3.3, Fixes #960).
A name bound to text literals, used as a String part of a tuple looked up in a container that holds text of its own (set.remove((1, other))), or kept in a published String field of a struct another file of the package declares, is declared String: the tier pass reads the package’s own published fields (ADR-282 D17, Fixes #956).
BTreeMap/BTreeSet (and HashSet) get extend and entry like HashMap/Vec (ADR-collections-1, #950); Array[T, N] gets iter() (ADR-collections-2, #955); the take-out-and-give-back rule of Part I 6.5 reaches let mut views (ADR-views-1, #949).A null in a tuple of a list literal whose declared element holds an option there takes that option, wherever in the list it stands (Fixes #957).
let c: Vec[(String, String?)] = [("a", "y"), ("b", null)] was NK1154; [(null, 1), ("y", 2)] under Vec[(String?, i64)] passed the check and was refused by rustc (the "y" not wrapped in Some).A pull parser over a moved text runs, and a tuple of String takes views, with no ref written (ADR-353 D8, ADR-282 D1-D2, #948, #929)
ref mut self method later hands back views of it that are still read: let mut p = Parser { input: text, pos: 0 } then p.next() in a loop lowers to Parser<'a> and Event<'a>, as Parser(text) through a constructor already did. A text parameter used only that way is lent by its callers. Where the field must own its text the move stays a move.mut and changes only other fields (next_item(c) changing c.pos) no longer counts as changing a moved text a view reads, so it is no longer a false NK2305. A change in place of the field itself is still refused at the move (D9). Such a mut parameter writes its inner lifetime for a view result (fn next_item<'a>(c: &mut Cursor<'a>) -> Option<&'a str>).String in a function’s or a grammar rule’s result takes the views it is handed (rule Line -> (String, String) = ind:text(" "*) body:until(line_ending) …), no longer NK1104. The text tiers follow a tuple into its parts; its String parts share one tier: a view, text of its own, or either per value.A match arm name bound by reference over a value (an option field of an .iter() loop variable) compares with a view or a value as it should, instead of Rust code rustc refused (Fixes #954).
let mut x = m.get(k) ?? <jump> over a mut map or list is a mutable view of the entry, as m[k] is (get_mut), instead of Rust code rustc refused (Fixes #952).
Vec, BTreeMap and HashMap of std; a copied value (a number, bool, char, a package’s copy type) stays a copy; an immutable receiver is NK1139, as for m[k].A text a live view still reads can be moved into a struct, tuple, list or variant literal (ADR-353 D8-D10, Fixes #942).
let moved = text; a struct returned with such a text lives in the caller’s keep.c.input.push_str(..) in advance, say) and the read of the view, and its help names .clone() or building the value first and taking the view from its field. NK1106 for a view stored in a field stays.A constructor that moves its text parameter into a view field lends it; a struct or enum result viewing a local takes the caller’s keep (#945, #942 steps 2 and 5; ADR-353 D8, D10).
fn new, pub fn(..) or a named method) whose result points into its text parameter writes that parameter with the struct’s lifetime and takes a lent argument: Parser(text) over a parameter works (Fixes #945).?? left of a comparison ((a ?? return false) == "x") keeps its parentheses in the lowering.ADR-353 D8-D10, decided (#942, #945): a text moved into a struct, variant, tuple or list while a view of it lives is kept, not copied.
NK2305 in D6’s words and names what forces it (a push_str in another function, say); the ways out are .clone(), or the struct first and the view from its field. NK1106 and NK2105 stay for cases with no live view.NK2305. Specified, not built yet.A view made from one field of a struct leaves its other fields free (#944).
parse(c.input)) is still used; a change to the viewed field, or to a path that holds it, is still refused.A text moved into a struct field, a tuple or a list while a view of it is still used is followed there: changing it in place is NK2305 (ADR-353 D3, D4, #942).
Cursor { input: text } (also the shorthand Cursor { input }), (text, 1) and [text] make the new local a root of the view; a struct field is watched on its own (c.input.push_str(..) is NK2305, c.pos = 2 is not), a tuple or list element as part of its container.E::A(text)) is not followed yet.A list field of text literals, drained and collected where Vec[String] is declared, keeps text of its own (#941).
collect over xs.drain() / xs.into_iter() links the list’s items to the collected list, as a list handed over whole is (ADR-282 D16, D29): a pub fn returning self.items.drain().collect() as Vec[String] is no longer refused with NK1104.**; A view result of two ref parameters is lowered with their lifetime (#655); ; rs2nika: if let Some(x) = &self.f becomes if self.f != null with x read as self.f; the let stays only where the block writes the field (ADR-382 D7); R48 leaves a non-name pattern with code before it to R23. Not yet: else if let, let-chains and && let in one-line closures stay TODO. Field row of selfhosting-gaps removed, ADR-382 built (#869); A field read through an index or a call is not a path: its NK1102 says so (ADR-382 D4); Messages and Part I name scalar, not char, among the types that copy, hash and order (#874); A value with no text form handed to a print call is NK1201, as in an f-string hole (#873); A view handed to a parameter the callee only reads is lent as it is (#872); A binding in a condition is refused with words that name match (ADR-382 D6) (#878); ; buffers: a clone of a cut of a text copies text, so a String moved into it stays owned (Fixes #881); check, emit: a struct field at a T? position narrows after its null arm (#886); check: a plain value in a tuple literal at a T? part is wrapped (#887); check: a proved name bound whole by a let or assignment that owns it is given up (#888); match: a text literal in a tuple pattern; a literal is the same part (#890, #890); keeps: a part moved out by ?? keeps its parameter, a tuple part as a field (#891); check: a value moved in an arm of an if or match is handed over (NK2105) (#892); check: a proved arm bound at a T? let or assignment is given up (#893); emit: an if/match lent whole with view arms and arms that make a value (#894); to_string() of a String parameter is handed on as the parameter is; check: an owned local in an arm of a lent argument is lent, not moved (#897 step 2); check: a variant’s or struct’s list-of-text part an arm takes is owned; a drained part asks no mut (#905); **
let h: ref E = given[0]).ref Vec[T] parameters returning ref T? or ref T is lowered with one lifetime over both parameters instead of dying in rustc.match over error names a type by its bare name or path and is complete when it names every type that can arrive, without else (ADR-357 D1, D5).null test of v[i].f or f().x proves nothing, and NK1102 at a use says the read is not a path; if let and is bindings in a condition stay refused (ADR-382 D4, D6).null test of v[i].f or f().x proves nothing, and the NK1102 at a use says the read is not a path (ADR-382 D4).Eq/Ord notes and the [v; n] note list scalars, not chars; Part I 1695, 1902, 2384 likewise (Nikaia has no char).print/println/eprint/eprintln of a value with no text form (a struct or enum of the program that is not an error, a list, map, set, range, tuple, SharedMut/Locked) is refused NK1201 before lowering, as f"{x}" already was; the help names the fields to print. Part I 2.5 says so; Part III NK1201 row extended.for element of a borrowed list, v[i], a ref binding) handed to an owned parameter the body only reads is lent as it is (ADR-094 D1) instead of refused NK1102; where the body keeps the parameter, NK1102 now says it is a view and the callee keeps it, and helps with .clone(). Part I 6.6 says so.String moved into another String variable beside a (x.strip_prefix(..) ?? "").clone() (trimmed or not) of it is lowered as a move; rustc refused it before (#880).String local moved into a variable that later takes x[a..].clone() of itself is lowered as a move; rustc refused it before (#881).null arm: in a match, F { name, .. } after F { name: null, .. } binds name as the T, as a tuple position does (ADR-352 D1, #886); fields of a plain struct pattern are now typed (they were unknown)T? part (argument, let, return, nested tuple) is wrapped as Some(..), as it is when passed to a T? parameter (#887).null and bound whole by a let, or assigned to a place that owns it, is the value itself, so let s: String = if b != null { b } else { "none" } compiles (#888).?? on a part of a lent tuple parameter (p.0 ?? "..") no longer fails in rustcif or match is handed over: reading it after the statement is NK2105, as after let s = t (#892).let or an assignment at a T? place is given up and the choice wrapped in Some(..), as return already did, instead of a borrowed &T that rustc refused (#893).if or a match handed to a lent parameter whose arms are a view and a value made in the arm now lowers to code that compiles: every arm is lent through the call..to_string() of a String parameter stored in a struct or a list hands the parameter on as the bare name does; both failed in rustc before.A list of text lent whole to a ref Vec[String] parameter keeps text of its own (ADR-282 D25)
ref list or map parameter of text holds owned text: a view kept in it is refused where it goes in (NK1105), instead of the generated Rust failing on a Vec<EitherText>. It reaches a list handed back from a function and one stored in a field; method parameters go the same way. Literals and static text are still built where they stand.Vec[ref String] where .clone() fits better; a tier walk that follows ref list parameters (the probe would then print as written) is a decision for ADR-282.A grammar takes values: rule parameters and options, bounds and guards that are expressions, count(p) an i64 (ADR-grammar-1 D2-D5, #932)
rule Block(n: i64; c: Ctx = Ctx::BlockIn) -> Yaml, called Block(n + 1) or Block(m; c: Ctx::BlockOut). An argument is a Nikaia expression over the rule’s values and the bindings before it, checked where it stands: a wrong count is NK1101, a wrong type NK1102, an unknown option NK1109, an unknown rule NK1171. A rule with values written bare is NK1101.Yml::block(text, 2; c: Ctx::BlockOut)." "{m}, " "{0, n - 1}, p{n,}, each an i64; integers written out stay what they were. To the analyses a bound they cannot evaluate has minimum 0 and no maximum.if(cond) and if(cond, "what was expected") fail the alternative without consuming where cond is false.count(p) yields an i64; the engine’s usize is converted where the grammar is lowered.until, list, count, …) takes patterns, any other name values; rule PATH = raw_ident PATH_TAIL* -> { } in tools/rust.nika is written without the group that now reads as a call. YAML’s block structure by indentation is a test (tests/compiler), as the engine’s own yaml_indent_test is.A text field taken whole from a struct a call made is moved, not viewed; comptime’s tree work in Nikaia (#125)
said = if out.ok { out.stdout } else { out.stderr } after let out = process::run(...) no longer puts out in a keep: a text field taken whole is moved out of its struct, and only one lent to a call (from_dump(res.out), res.out.trim()) is a view the struct must outlive. Since 0.0.1258 every such field counted as a view, and rustc refused the move with E0507 (tests/guide, the accepted-programs sweep).comptime_run moves into Nikaia (tools/comptime_tree.nika).A grammar action stands after -> again; a brace after a pattern is a bound (ADR-grammar-1 D1, #932)
rule pair -> Pair = key:string ":" => val:value -> { Pair { key, val } }: each alternative’s action follows its own ->, as the engine writes it. pattern { action } is refused by name with the form to write; an arrow elsewhere in a pattern still names =>. A brace after a pattern is a bound wherever spaces stand (digit{2}, digit { 2 }), so no space decides what a grammar means. This withdraws ADR-296 D33.{ p } as a delimiter in the input (Part II 10.8) is withdrawn; it was never built: write "{" p "}".scripts/grammar_arrow.nika); the lowered Rust does not change. ADR-grammar-1 also records what #932 builds next: rule values (Subject ; Config), value bounds, if(cond) guards, count(p) as an i64.A view of a list in a field is a slice, as a parameter’s is (#125)
ref Vec[T] is lowered to &'a [T], as a ref Vec[T] parameter already was (#458): a method that hands a parameter’s view to the struct no longer fails in rustc with &Vec<T> against &[T]. Found by rs2nika’s check loop on a walk that keeps its inputs in fields.winnow-grammar 9b15c55 and winnow 1.0 (#932)
winnow follows, so one version is in the tree. Nothing a Nikaia grammar can write changes yet: the Nikaia side of ADR 26 is #932.A list, a map and a walk have what converted code calls (#125)
std gains the entries rs2nika’s drafts called and did not find: Vec::get(at) -> T?, HashMap::iter, BTreeMap::values and BTreeMap::iter (pairs of views, (ref K, ref V)), and Seq::enumerate ((i64, T), the position an i64 as a count is), Seq::chain and Seq::fold.get, first or last of a value that copies is the value (.copied() below), so let x = xs.last() ?? return compares as a number; of a tuple it is a view of the element, so let (name, n) = it and an arm (name, n) => over it read n out of the view.to_string below; text a fold starts from is text of its own.Seq::peekable, which needs a type of its own.A lambda’s parameter takes a tuple apart (ADR-358 D1, D2, D5; #623, the lambda half)
pairs.iter().filter(fn((name, n)) { n > 0 }): a lambda’s parameter may be a tuple pattern, nested as a let’s is (fn(((a, b), t))), beside plain names. It is one parameter, under a name of the compiler’s (__nikaia_tuple and the byte it starts at), taken apart by a let first in the body (unpacked, tree_build.nika), so its parts are typed, lent and read out of their views as that let’s are. A pattern of the wrong width is NK1283, named as a lambda’s parameter.NK1101’s help for fn(a, b) handed one tuple writes the pattern: fn((a, b)) { … }.ref / mut before it (D3, D4).A mut parameter is a source of the result’s views; text given to a ref String field is lent (#929, ADR-283 D25, ADR-282 D2)
mut parameter (fn next(mut c: Cursor) -> String? returning a piece of c.input) points into that parameter, as a method with ref mut self does: the ledger writes ref(c) and the lowering ties the result to it instead of 'static, which rustc refused where the struct owns text read while the program runs.P { field: text } where the field is written ref String lends text, as ref text does: the field says what String says (NK1261), so the literal is no longer refused with NK1106.view_changes in Nikaia (ADR-294, #125; ADR-353 D3, D4)
crates/nikaia/src/view_changes.rs to tools/view_changes.nika; the Rust file is a thin adapter. The keep plans and give-ups of every lowered file are unchanged. First draft by rs2nika --engine tree.An argument made in a call to a function with a keep of its own is the caller’s to keep (ADR-283)
parse(parts.join("\n"))) is kept by the caller, beside the call or in the keep it was given (made_arguments, buffers.nika).A keep for a struct of views a call makes, across the files of a package; text of either kind looks a key up (ADR-283, ADR-282 D14)
to_string of it is text of its own. A call that needs a keep, written inside another call that makes text (count(parse(text))), gets one. A struct of views declared in another file of the package counts as one here, both for the keep plans and for the ledger (keep::beside). An opaque value put into a keep is put as itself, never through a view (Keep::put).named[name], BTreeMap and HashMap).std::regex: run-time patterns without positions; a match over error names a type by its name (#673, ADR-regex-1, ADR-357)
a catch block that begins with an arm is a match over error (ADR-catch-1)
A file’s canonical name and a path’s prefix in std::fs
std::fs gains fs::canonical(path, root), the absolute name with links, . and .. resolved (NotFound for a missing name, Outside where the result leaves a Dir store), and fs::Path gains strip_prefix (on whole parts, no file system read) and clone (Part III 17.1, ADR-319 D3).std::regex typed by its engine; a function specialised per build-time value: comptime?, @unroll, decided branches (ADR-regex-1, ADR-specialization-1, #673, #906)
Node ids from a pass of their own; owned parts and parameters; ?. on a file’s name
b.xs.drain()) keeps the parameter: it is lowered owned, not as a reference whose field cannot be given away.?. on a file’s name calls std’s path methods.std::regex decided: RE2 dialect, known patterns compiled as source, run-time patterns on Rust’s automata crates (ADR-regex-1, #673)
null arm (#902)In a match, R::V(name) after R::V(null) binds name as the T, as a tuple position and a struct field do (ADR-352 D1): the earlier arm is the same variant, unguarded, and at each other value takes all or says the same. A variant or a literal at such a value is matched against the T. A pattern with .. among its values is not narrowed yet.
opt ?? "" is text too (#901)(line.strip_prefix("for ") ?? "").split_once(" in") ?? ("", "") gives two texts like a cut of a named text: a .clone() of a part keeps no view of line, so line stays movable (the converter’s for X in line, R217).
split_once that is cloned keeps no view of the text it was cut from (#901)The two parts of a split_once/rsplit_once of a text (also with ?? ("", "")) are text: a .clone() of one keeps no view of the text cut, so that text is not parked in the frame’s keep and a later move of it (out.push(line)) compiles.
replace hands back text of its ownThe result of replace and replacen is text of its own, not a view of the text they are called on, so s = out.replace(a, b) no longer puts out into the frame’s keep, and a pattern built in a loop and handed to replace stays an owned String (found in the converter; &&String was no pattern, and a kept buffer could not be pushed to).
A value moved in an arm of an if or match is handed over: reading it after the statement is NK2105, as after let s = t (#892).
A text literal at a String or String? position of a tuple pattern is matched against a view of that position, so match (a, b) { ("x", s) => .., .. } compiles (#890). A literal at a tuple position counts as the same part for narrowing (ADR-352 D1): (1, s) after (1, null) binds s as the T, and a different literal does not.
return match is given up, as at the tail of a function (#890).let is the value itselfA name proved not null and bound whole by a let, or assigned to a place that owns it, is the value itself, so let s: String = if b != null { b } else { "none" } compiles (#888).
place/floor move into Nikaia (E12-3b, #583).?? on a part of a lent tuplep.0 ?? ".." on a part of a lent tuple parameter no longer fails in rustc: a part moved out by ?? keeps its parameter, and a tuple part is treated as a field (#891).
null armIn a match, F { name, .. } after F { name: null, .. } binds name as the T, as a tuple position does (ADR-352 D1, #886); the fields of a plain struct pattern are now typed (they were unknown).
read_with’s loops move from emit/seeds.rs to tools/emit_seeds.nika (E12-3a, #583).T? part is wrappedA plain value in a tuple literal at a T? part (an argument, a let, a return, a nested tuple) is wrapped as Some(..), as it is when passed to a T? parameter: let p: (i64?, String) = (1, "a") compiles (#887).
catch on a call used as a statement drops the call’s valuesave(file) catch { log("not saved") } no longer needs its block to hand back what save returns: a catch whose value is not used (a statement, a loop body’s tail, the tail of an arm whose value is dropped) has a block that runs for its effect, and the call’s value is dropped (Part I 7.1, #885).
What a match guard proves holds in that arm’s body, as after an if (ADR-382 D2): in (x, _) if x != null => x, x is a String, && in the guard carries its proof, and no other arm sees it. In a match over a tuple, a bare name at a T? position binds the T once an unguarded arm before it has null there and covers the other positions (ADR-352 D1): after (null, null) and (null, y), (x, _) binds x as the T, and y is set too. A variant or literal at a T? position is matched against the T. A name proved not null that a function hands back whole, by return or as its tail, is now the value itself, also for text; it was lowered as a reference, which rustc refused (#879).
catch whose block hands back null gives T?let port = read_port() catch { null } is an i64?, the way a ?? fallback or an if arm with null widens. A value that is already T? stays T?. A block that hands back another type is still NK1103, and its help now says “Hand back an i64, or null to make the value an i64?” instead of telling you to change a declaration a catch doesn’t have. A catch may now also stand in the head of a match, if, while or for: match parse(line) catch { null } { v => use(v), null => {} } (#884).
A String local assigned whole from another kept String local compiles
(#883). When the result was the trimmed view of the source local, the keeps
walk put the assigned local in the keep a second time, and rustc refused the
lowered code; it is now kept once (tools/buffers.nika). Test
string_reassigned_trim_result.
A Bytes has iter, starts_with and ends_with (#877). A Bytes,
s.bytes among them, has iter() (a Seq[ref(self) u8], as Vec::iter),
starts_with(prefix) and ends_with(suffix) over a ref Bytes; they were
NK1171 before. Test bytes_iterate_and_compare_their_ends.
A String parameter read through find stays lent (#882). The keeps
walk (tools/keeps.nika) looked a String receiver’s method up as
String::find; the ledger names text methods str::, so it fell back to
every find, Seq::find among them, which takes its receiver. Passing a
text to a function that only searches it no longer hands the caller’s text
over (NK2105), a for element passes (NK1102), and the slice after it
lowers to &str. A String? read through ?? or match is still kept
(#882 stays open). Tests a_text_parameter_read_through_find_stays_lent,
a_for_element_is_lent_to_a_text_parameter_read_through_find.
find, rfind, replace and replacen take a scalar needle (#877,
Part I 2.6). s.find(',') was NK1102 while contains and split took a
character; a needle is now a text or one scalar for all of them. Test
a_text_search_takes_a_scalar_needle.
Two ref Array[T] compare with == where their elements do (#877,
Part I 4.7). A view of a run was refused with NK1188 whatever it held; it
now compares as the run does. A lent pointer of the C boundary still does
not. Test two_byte_views_compare_by_their_bytes.
A clone of a cut of a text copies text (#881). A String local that
starts as "" and is moved into a variable that later takes
x[a..].clone() of itself was lowered as a view, and rustc refused the
move; it is lowered as a move. Test string_empty_then_moved.
A clone of a trimmed x ?? "" copies text too (#880). copies_text
(tools/buffers.nika) now also counts (x ?? "").trim().clone() and its
kin as a copy, the form rs2nika’s R92 writes for a map’s entry, so a
String moved beside it stays owned. Test string_move_not_view.
A String moved after a clone of x.strip_prefix(..) ?? "" stays owned
(#880). The lowering’s text-copy check (copies_text, tools/buffers.nika)
did not count (x ?? "").clone() as a copy, so the moved String kept the
origins of the text it was cut from and was lowered as a view, which rustc
refused. A clone of a ?? b now copies when either side does. Test
string_move_not_view.
A binding in a condition is refused with words that name match (#878,
ADR-382 D6). if let v = x, while let v = x and if e is Leaf(v) now read
“A condition can’t bind a name”, with the help match x { ... } or x != null,
instead of the reserved-word message (let_) and “Expected {”. Tests
an_if_let_binding_in_a_condition_is_refused,
an_is_binding_in_a_condition_is_refused, a_while_let_binding_is_refused.
A comptime number below i64::MIN is NK1116; the compiler builds no
i128 (#871, ADR-285 D19). comptime BIG = -9223372036854775807 - 5 was
carried as an i128 and reached rustc; it is refused at the declaration, as
a value over u64::MAX already was. An integer literal is a magnitude and a
sign (int_literal(value, negative)), and int_value is gone, so the
self-hosting gap row “i128 has no twin type” goes. Test
a_comptime_number_below_i64_min_is_refused.
A T? handed to a print call is NK1102 (Part I 2.3). println(x) with
x: String? reached rustc and failed there with an Option display error;
it is now refused as the f-string hole already was (f"{x}"): what an absent
value reads as is the program’s to say, println(x ?? "none"). Built in
tools/check_refusals.nika (a_maybe_printed), called from the print branch
of tools/check_arguments.nika; test
a_maybe_handed_to_println_is_refused. The syntax tour shows it.
ADR-382 replaces ADR-350: a null test narrows a path of fields too.
Decided, not yet built (Part I 2.3):
p.name, self.cache,
a.b.c) is proved not null and taken back like a name (D4). v[i].f,
f().x and a lock’s value are not paths.mut hand-over (a ref mut self method, a mut argument), or a
lambda, task, overlap branch or select arm that sees the root. A sibling
write and a ref call keep it. NK1102 names the write.match only: if let and if x is v are
refused (D6). let x = self.f stays, needed only to keep the old value (D7).docs/history/adr, and every citation points at ADR-382.lending fixes, views of locals, one statement a line, operator types, seeds tables in Nikaia (#655, #665, #848, #864-#867)
Vec[ref E?] or written as a part of a (i64, ref E?) tuple is lent and goes inside its option (Some(&..)) instead of failing in rustc or NK1103 (#655)mut parameter read through ?? return is lent, not moved (rustc E0507).bool, - on a non-number and ! on a non-bool non-integer.let over a field of a by-value parameter, then handed back or pushed, is a move; the contract keeps the parameter when the alias is handed on+ can be returned from a String function; the local lives in the caller’s keep (#864).to_string() of a number expression (len(), arithmetic, bool, a call returning a number) may leave the function.pub structs’ map positions, the barrier closure and the self-hander test move to tools/emit_seeds.nika (EmitSeedsTables::count_types, close_barriers, self_handers, hands); the Rust keeps the AST walks as collectors of plain facts (#867)owned literals, NK1301, emit a_stmt in Nikaia (#856, #853, #857)
Part III 17.3: json::parse and json::Value (#604)
json::parse reads into a json::Value, numbers stay text and convert when asked for as a type (ADR-344 D6a).integer as scalar, match arm naming a struct type (#604)
as scalar; the cast aborts if the value is not a Unicode scalar value.match arm that names a struct type by a path matches the type whatever its fields (json::Error =>).process::run within, option defaults, map get pair view (#846, #847, #845, #675)
m.get(k) ?? ("", 0) over a map of (String, i64) or (String, String) pairs now compiles and answers the pair with the text as a view (#846; #845 and #675 needed no change, tests only).std::process::run gains within: and Output gains timed_out (ADR-243 D9, #847 step 1).T? = null is now wrapped in Some, and the default null is None.emit: a kept Bytes follows the receiver’s method; grammar patterns in Nikaia (#756, #842, #583)
ref Bytes parameter now takes it as &Bytes, and its call sites hand over the Bytes with its owner (ADR-373 D1, D4): a Bytes or a mapping is not copied, and a run of bytes such as a text’s is copied once (#756).Bytes over with its owner only where the method its receiver’s type resolves to keeps it; a same-named method elsewhere, or a closure in a field, no longer changes the call (#842).tools/emit_pattern.nika); the grammar’s bytes flags live in the walk (#583).EitherText for an assigned if/match, the emitter’s item writer in Nikaia (#841, E11-1)
let lends a field read through a ref, a let-bound function lends its parameters, NK1131 alone (#744, #839, #582, #747)
compiler bugs and –cpu with RUSTFLAGS (#792, #777, #773, #760, #787, #757, #829, #762, #764, #765, #758, #830, #831, #832, #776, #799, #834, #779, #790, #835, #836, #837, #753, #780, #838)
Some(x)/None over T? refused; list elements past a jump are views; T? slots widen (ADR-352 D5, #803, #819, #824, #827, #828)
v[k] ?? return) over a list of options is bound as a view, in untyped let (#819), typed let and call arguments (#827); where an own value is wanted it is refused with NK1103/NK1102 instead of rustc E0507.let ts: Vec[String?] = ["abc"] can be passed as ref Vec[String?].String::quoted(); panic(..) works in tools; methods are matched by receiver type in keeps (#804, #805, #791)
s.quoted() returns the text between double quotes, escaped as Rust’s {:?} writes a string: " and \ get a backslash, newline/tab/carriage return become `
` \t \r, other control chars \u{..}, every other char (e.g. ä) stays; a std method with a contract entry, no new error code.nikaia_std::abort::panic in its package preamble, so panic(..) in std’s Nikaia sources compiles instead of E0423.fmt_ method prefixes.Check: the kept-function call, the findings order and the unsigned-slot verdicts are Nikaia (C7-a to C7-c, #125)
Check: the walk’s settling and the unrolling report are Nikaia (U-d6, #125)
Self-hosting: the checker’s program walk is Nikaia (U-d5)
The lowering cache is keyed by the compiler’s source fingerprint, not by the package version (#801)
self-hosting: the checker’s function walk, grammar action walk and growing-text scan are Nikaia (U-d4)
Proofs resting on an assume are marked in the reports (ADR-381 D6)
binary_search requires a sorted list; sort ensures it (ADR-381 D5)
Named properties: a pure bool call is proved by name (ADR-381)
lower-std: one generated Rust module per tools file
nikaia lower-std writes tools/lowered.rs plus tools/lowered/<file>.rs (and runtime/lowered*), replacing the 73k-line package.rs (7.4 MB single unit, now 214 files, largest object 1.66 MB); a change to one .nika recompiles one Rust module (one of 256 objects in the incremental session changed, 78,216 bytes); two sessions changing different files no longer conflict in generated code; cross-file visibility by a lexical pass making top-level items pub(crate); a cold run equals a cached one byte for byte. Also this round: docs/ci-cache.md and issue #795 (CI time shares), docs/long-jobs.md.Self-hosting: the checker’s defaults, item constants and folds are Nikaia (U-d3)
comptime constants and grammar folds are checked in tools/check_defaults.nika (252 code lines); CheckHost loses computed_attribute and gains type_of and constants_beside; check/mod.rs shrinks by 506 lines; behaviour unchanged; the self-hosted share is 69.1 % (71944 lines).f(1, *) because a trailing comma in call arguments parses since the formatter (ADR-337); the type_rules test a_literal_is_proved_or_checked expects no check in ring because slots.len() == CAP is provable.tools: attributes check moved (U-d2); lower-std caches each file of a package (#667, #125)
nikaia fmt formats a package, --check reports
nikaia fmt [FILE...] rewrites the package’s .nika files, or the named files and directories, in the ADR-337 style and names each file it changes. --check writes nothing, names each file that would change and exits 1. A file the guard refuses is named with the reason, left untouched, and makes the exit code 1.nikaia fmt: the printer and its guard; a trailing , after parameters and arguments
, after the last parameter, option and call argument (ADR-337 D1). nikaia::fmt::format prints a file in the one style of ADR-337 D1-D3 and writes nothing if the result does not parse, would lose or move a comment, or would change the tree (D4). The nikaia fmt command follows.The repository’s unsafe crates ship their reviewed ledgers
nikaia.contracts that nikaia describe adopts (ADR-290 D22). Each lists only the crate’s safe, non-generic free functions, with any cfg gate in a comment; an unsafe fn stays NK1171. examples/crate-ledger calls crc32c::u64 and low_bits through use. Fixes #788.use names a Rust crate; a crate may ship its reviewed ledger (ADR-290 D22)
use <crate> resolves a type = "rust" path dependency. A crate may ship nikaia.contracts beside its Cargo.toml; nikaia describe adopts it after checking its # crate: version and source hashes, and refuses a stale one (#788).nikaia run applies the user’s linker and flags again
std is compiled without Cargo only when no Cargo configuration Cargo would read (from where the tools start upward, then CARGO_HOME) sets [build] rustflags or a [target] linker or rustflags, and no RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS, CARGO_BUILD_RUSTFLAGS or CARGO_TARGET_<TRIPLE>_LINKER/_RUSTFLAGS is set; otherwise Cargo builds it and applies them (ADR-325 D3, #786).A method argument that lends the receiver’s local to a mut parameter is bound before the call
w.add(g(w, 1)) lowered to two &mut borrows of w (E0499); the argument is now bound first, together with every earlier computed argument, so effects still run in the order written (#785).An argument that reuses a mut argument’s local is bound before the call
f(w, g(w, 1), 2), where a later argument computed in the call mentions the local lent to a mut parameter, binds that argument (and every earlier computed one, in order) to a let before the call, so the generated Rust no longer fails with E0499. Method receivers (w.m(g(w))) are not covered yet.The prover follows the length of a mut list
mut list’s v.len() is a mutable whole number to the prover (ADR-361, #782): known after a list literal or [x; n], set by push, clear, resize and truncate, and forgotten after any other method that may change it, a hand-off to a function with a mut parameter, and at a join or a loop that may change it. An index proved against the followed length needs no unproved-claims relief.check: --asserts lists an unproved claim as refused (ADR-380 D1)
--asserts lists a claim refused as not proved (NK1300) as refused, with its reason (ADR-380 D1).Unproved asserts refused: NK1300, unproved-claims (ADR-380)
assert the compiler cannot prove (neither proved nor shown false) is refused with NK1300 by default; [build] unproved-claims = "warn" or --unproved-claims warn makes it a warning and the claim is checked at run time. Not asked: proved claims, preconditions every call proves, main’s, a test’s, an assume.slots.len() rather than a field of a new binding.An unconditional write before the ifs that cannot tell (built)
ifs directly in front of it where nothing can tell: they neither read nor write what it writes, it reads nothing they write, neither side calls out or can stop the program, the ifs do not leave, and it is not the block’s last statement; in every build (ADR-379, Part I 8.1.1, #781). A let bound to a call’s whole-number result is a number of the bounds walk. 1BRC fast-unsafe.nika in its natural order: 88.0 -> 84.0 instructions a row at x86-64-v3, 102.0 -> 98.0 at x86-64 (1 unit).A field through one element is a name of the walk
t.slots[at].sum), is a name of the walk as t.used is, until the name changes or a write may reach the same fields of any element (ADR-306 D20, #781 step 1). unsafe { assume(t.slots[at].sum <= B) } now proves t.slots[at].sum += v.An unconditional write before the ifs that cannot tell (decided)
ifs directly in front of it, where none of them touches that place or what it reads, neither side calls out, pauses or can stop the program; in every build (ADR-379). Decided, not yet built.The emitted code is unsafe only where a switch or the program says so
remove-* switch off the emitted code holds no unsafe but the program’s own; each check a switch drops is a call into nikaia_std::proven (ADR-378). A proved shift is proven::shl/shr instead of <T>::unchecked_shl, which also kept the emitted code above Rust 1.88, and a proved negation proven::neg instead of wrapping_neg; each asserts its dropped check in a debug build.A proved +, -, * tells the optimiser it does not overflow
+, - or * proved to stay inside its type lowers to nikaia_std::proven::add::<T> (sub, mul) from proven-index, unchecked_* behind a debug_assert, instead of <T>::wrapping_* (ADR-306 D19). A wrong proof stops at the assertion in a build that keeps debug assertions and is undefined behaviour in one that does not, as a wrong proved index or shift already is.x += d and x ?? d keep what bounds them
x += d on an untyped let mut x = 0 takes d’s type and is proved as x + d (ADR-285 D23, ADR-306 D9); x ?? d lies where the number in x and d do, and what a callee’s ledger entry ensures of result is said of the number its option holds; simd::Mask::first() ensures 0 <= result < 64 (ADR-377 D7). The bounds walk guesses a value’s bounds from what the ledgers ensure as well as from the facts.simd::splat(';') into byte lanes is a byte
simd::splat for u8 lanes is typed and lowered as a byte (b';'), as everywhere a u8 is wanted; it was lowered as a char, which rustc refused (#773).Mask.first() of byte lanes is one pmovmskb
Fixed[u8, N], Fixed[i8, N], N of 16, 32 or 64) gathers its lanes into bits with one pmovmskb a 16 or 32 lanes (byte_blocks::high_bits), and first() is the lowest bit; other masks gather their bits in one pass instead of searching lane by lane. A 1BRC ; search with simd::Fixed[u8, 32] costs 96 instructions a row instead of 362.low_bits cuts by a count up to 255 with one bzhi
low_bits is held to 0..=255 for bzhi, as BitCount::index documents, not to 0..=127: a count the program already knows to be at most 255 needs no clamp above, so a name cut to its 16 bytes costs one bzhi a word.find_all takes how it goes from one hit to the next
b.find_all(set; scan: bytes::Scan::AfterHit) looks for each hit in the half block right after the one before; Scan::ByBlock, the default, is again the search before 0.0.1184 (ADR-375 D6).find_all looks for the next hit right after the last one
FindAll::next compares the half block right after the last hit and reads whole blocks only where it holds none, so that the test whether a hit was found is predicted where hits are near one another; byte-blocks masks a block or half of one.The prover reads if expressions and comptime conditions
if expression lies in the join of its arms’ ranges, each arm under its condition; an if on a module comptime of type bool has only the facts of the arm the build takes.target::has, built
std::target::has(target::Instruction::Crc32c | Bmi2 | Avx2) (ADR-371 D6) answers for the --cpu level the program is built for, the same in a comptime and at run time; without --cpu the baseline’s answer, only --cpu=native asks the building machine. A comptime that reads it, and the lowering, are kept per level (#778).target::has, not cpu::has
target::has(target::Instruction::X), a question to the build’s target, not to the CPU the program runs on; without --cpu it answers for the target’s baseline, never for the machine that builds. Owner’s direction.target::has: a program asks its CPU level (decided)
target::has(target::Instruction::Crc32c) answers whether the CPU level the program is built for (--cpu) has an instruction, in a comptime for that level and not for the machine that builds; Crc32c, Bmi2, Avx2 first. Not built yet.hash::crc32c(crc, word)
hash::crc32c(crc: u32, word: u64) -> u32 is one CRC-32C step over a word (ADR-371 D5, Part III chapter 16): one crc32 where the build has SSE 4.2 (--cpu=x86-64-v3, through the new crates/unsafe/crc32c), a table at the baseline, the same value on every target.find_all’s offset never passes the end of the run
std’s find_all keeps the run and an offset again (0.0.1176 had made it a slice cut from the front), now one that never passes the run’s end: the last, short block’s bits are moved to the top of a block that ends where the run does. The bytes left are the run’s length less the offset, without a conditional move, and a position stays offset arithmetic the caller can fold. 1BRC, 1 M rows, 1 unit, against 0.0.1175’s find_all: fast.nika 107.4 -> 104.7 at x86-64 and 103.4 -> 103.7 at x86-64-v3, fast-unsafe.nika 99.6 -> 97.9 and 96.6 -> 94.8.a proved shift is written as the shift that may assume its count
unchecked_shl/unchecked_shr, as a proved index is read without its test, instead of wrapping_shl/wrapping_shr: the masked count hid the count’s range from LLVM, so x & (ONES >> (64 - 8 * n)) did not become one bzhi. 1BRC’s fast-unsafe.nika at x86-64-v3, 16 units: 95.9 -> 93.7 instructions a row.find_all cuts what it has not covered from the front
std’s find_all keeps the bytes no block has covered as a slice it cuts with split_first_chunk, instead of finding them again from an offset at every block: one length test a block and no conditional moves. 1BRC’s fast-unsafe.nika at x86-64-v3, 16 units: 0.9 instructions a row fewer.a map compares text keys by words
std’s own type over hashbrown’s table (ADR-296 D42). A text key is compared a word at a time with no memcmp, hashed inline up to 16 bytes, and found with the probe inlined into a fold; the hash values and the iteration order are unchanged. 1BRC: 235.0 -> 209.2 instructions a row at 1 unit, 222.9 -> 208.8 at 16.a proved index is a place without its check
proven::slot without its check, as it is read (ADR-306 D5).x.low_bits(n) is one bzhi where the build has BMI2
--cpu=x86-64-v3 (BMI2), low_bits on u64, u32 and u8 lowers to one bzhi with the count saturated to a byte, through the crate crates/unsafe/low-bits (ADR-371 D2); other builds keep the branch-free mask and select. The values are the same at every CPU level.the grammar runtime’s advance always inlined
advance is always inlined: examples/1brc.nika 244.0 -> 235.0 instructions a row at 1 codegen unit, unchanged at 16 (#695).x.low_bits(n)
u8, u32 and u64 gain low_bits(n): x with only its n lowest bits kept, none where n <= 0, all where n is at least the width; the count may be of any integer type, so no count aborts. Lowered branch-free, a mask below the count and a select.a method’s precondition is proved at its call
self.m(…) by the prover, at any call on the program’s own type by the bounds walk; a proved call skips the entry check (ADR-269 D20). Bytes compares equal with a cut of bytes. Decided: ADR-361 D11’s summary of a field of every element is carried across a fold’s step and merge, as ADR-367 carries a counter (#770).find_all over a written set fixed at compile time; a believed assume’s arithmetic exact; cuts compared in place; low_bits
find_all over a set written as one to three byte literals is fixed when compiling, with no dispatch per 64-byte block. The arithmetic of a believed assume is exact: + - * lower to unchecked_* (ADR-376 D4); a checked claim keeps its checks. Two cuts compared, a[i..<j] == b[k..<l], compare in place a word at a time. Decided: x.low_bits(n) keeps x’s n lowest bits, for u8, u32, u64 (to be built).a field of every element of a list as one summary (decided)
prove-through-loops:on, a whole-number field of the elements of a list a loop writes through an index is one summary for all elements (weak updates), related to the loop’s counters by f <= c + n and |f| <= K * g (ADR-361 D11, #770; to be built).facts at a call; signed shifts bounded; rotate_left and rotate_right
assert before return) are known of the call’s result to the bounds walk; the prover reads e & c and (e >> k).bits_as_i64() as bounded and keeps that through a loop that assigns only such values; panic(…) is no exit; a Bytes parameter has a length the prover reads (#776). A signed x >> k of w bits lies in -2^(w-1-k)..<2^(w-1-k), and ^ keeps -2^m..<2^m. rotate_left(n) and rotate_right(n) on u8, u32, u64, the count taken modulo the width. nikaia lower x.nika at a project’s root no longer refuses the file as declared twice (#774).vectors a program holds, built: simd::Lanes, simd::Fixed, simd::Mask
std::simd (ADR-377 D1-D7, #775): Lanes[T], Fixed[T, N], Mask[T] / Mask[T, N]; simd::load, simd::splat, simd::Fixed.from, simd::select take their type from what receives them (NK1245 without one); v.count() is the lane count; lane arithmetic named wrapping_*/saturating_* (+ - * / % NK1296), shifts by a literal below the lane width (NK1299); dot_pairs saturates u8xi8 into i16 and wraps i16xi16 into i32. A load proved in range has no check. nikaia run --cpu=LEVEL builds through Cargo at that level.a fold’s hot loop inlined alike at any unit count; text used as a map key checked on a miss
#[inline(always)] (ADR-296 D41), and std’s text hash is a call of its own, so a map’s entry is inlined into the loop at 1 unit as at 16: 1BRC 273.9 -> 244.0 instructions a row at 1 unit, 231.3 -> 222.9 at 16 (#695). Decided: over a Bytes, text a grammar yields that the program uses only as a map’s key is checked as UTF-8 where the map misses (ADR-366 D5, #664; built next).vectors: simd::Lanes and simd::Fixed (provisional)
std::simd’s (ADR-377, provisional): Lanes[T] at the build’s widest register, Fixed[T, N] at a fixed lane count; lane arithmetic is wrapping_* or saturating_* (+ - * is NK1296); Fixed alone offers lane, shuffle, dot_pairs, widen, narrow_saturating; a Mask is read as positions. Decided; built by #775.a type’s rule a method cannot break is proved; the bounds walk bounds shifts, bounded sums and break guards
self reads a field the rule does not read, counts or indexes a field whose length it reads, or calls the type’s own methods) does not check it; after a changing own call the rule is a fact again (ADR-369 D2, D3, #714). The bounds walk bounds an unsigned x >> k below 2^(w-k), proves l + x from l where x lies between two constants, takes !c of if c { break } as a candidate loop invariant, keeps the length of what find_all searches, and reads a whole-number field bound by let through an index as a number (ADR-361 D4). Lowering: a compound value put into a nullable field is converted as a whole.a checked assume is no Rust unsafe
unsafe block of nothing but assumes lowers to a plain block around the asserts’ checks, so rustc no longer warns of an unnecessary unsafe; with checks removed it stays Rust’s unsafe around assert_unchecked (ADR-376 D2, #772).prove-through-loops; a ref argument keeps its length
prove-through-loops:on (ADR-361 D10, #767): an optimization word and --optimization flag, off by default. Under on, a loop that leaves open an index, overflow, shift, negation or unsigned-slot check reading a name the loop changes is walked again with a candidate invariant, taken from the facts the loop is reached with; each is proved at the loop’s entry and at every end of a turn, continue and break (D4), and kept only where it proves more. The invariant is kept in nikaia.proofs (D7). std, the tools and the checker never use it, so what is accepted does not depend on it. A ref argument keeps its length facts across a call; only a mut parameter changes them (#771).unsafe { assume(claim) }
unsafe { assume(claim) } (ADR-376, #772): written only inside unsafe (NK1294), never in std (NK1295), read as an assert’s claim and a fact from where it stands. Checked as an assert where the build removes no checks; lowered to core::hint::assert_unchecked where remove-bounds-checks or remove-overflow-checks is on. --asserts lists every assume. Part I 1.3 lists it in the prelude.A grammar’s par_fold names winnow_grammar’s ParseContext in full
par_fold with options lowered &ParseContext::<()>::default() without the use that names it, and rustc refused it (CI’s accepted-sweep, a checker test’s program); the call now names winnow_grammar::ParseContext in full.unsafe { assume(claim) } (ADR-376, decided); a type’s rule another package can break is no fact (ADR-369, built)
assume(claim) inside unsafe is a fact from where it stands; checked as an assert in a build with remove-bounds-checks:off and remove-overflow-checks:off, believed (lowered to core::hint::assert_unchecked, undefined behaviour if false) in a build with either on; outside unsafe NK1294, in std NK1295; listed by nikaia check --asserts. Decided, not yet built.pub field, or a pub type every field of which is pub or defaulted, no longer takes its rule as a fact at function entries: a dependency is lowered without its consumers in view, and remove-bounds-checks:on read past a slot a consumer had removed. A literal of a type built in another file of its package is checked against the type’s rule.A search’s positions and a shift’s count in the bounds walk (ADR-375 D5, ADR-306 D18, built)
std.contracts gets an ensures_each key; find_all’s positions satisfy from <= s < b.len() in a for, also through a let, not after filter or map. A std method’s ensures on its result is read too (trailing_zeros/leading_zeros are 0..=w). A shift whose count is not a literal asks 0 <= k < width; a proved one is written as wrapping_shl/wrapping_shr under remove-overflow-checks:on. Refusing a count shown outside waits for D15’s refutation.A type’s rule is a fact only where nothing can break it unchecked (ADR-369 D2, D3)
self and throws, or whose fields or literals another file of the package touches, keeps no entry fact (both were reads past the end under remove-bounds-checks:on); a failure names the type (“the rule of T: …”); a generic struct’s rule is refused as generic (NK1288).A type’s rule: an assert after a struct’s fields (ADR-369 D1-D3, built in part)
assert(claim; message: …) after its fields is checked or proved at every literal, before every exit of a method that changes the value and before such a method hands self on; at each entry it is a fact for self and each parameter of the type, so a masked index into a field is proved. A rule that is not an assert over fields, paths, pure methods, comptime values and literals is NK1288. Writes from outside the type’s methods and with copies switch the entry fact off; D4 and D5 are not built.@inline is built (ADR-370)
@inline, @inline(always) and @inline(never) before a function or method lower to Rust’s #[inline], #[inline(always)] and #[inline(never)], on both entries of a function with a precondition; a written one replaces the hint on a fold step’s callee, so @inline(never) wins. Refused before anything else, a trait’s method included (NK1235), written twice (NK1236), or with any argument but always or never (NK1293, new).A shift’s count is a goal of the walk (ADR-306 D18, decided)
0 <= k < width as an index asks its bound; remove-overflow-checks:on writes it without its check where that is proved, and a count shown outside is an error as D15 says. Decided, not yet built.A position find_all yields is a fact for the proof (ADR-375 D5, decided)
b.find_all(set; from: f) yields is f <= s < b.len() for the walk, stated in std’s ledger as ensures_each on the sequence’s elements and read on the name a for binds. Other searches take the same entry only when a program needs it and an assert cannot give it. Decided, not yet built.prove-through-loops: the compiler’s own checks asked through a loop (ADR-361 D10, decided)
prove-through-loops:on asks an index’s or an overflow’s check that reads a name a loop changes, and that ADR-306’s walk leaves open, once more through the loop’s invariant, as an assert is asked; it changes what is proved, never what a program means. off by default. Decided, not yet built.The bounds walk: a u64 is not bounded by i64::MAX; facts survive a bound as wide as i64
u64 is no longer assumed to be at most i64::MAX: (k.bits_as_u64() >> 56) was taken as at most 127, so with remove-bounds-checks an index into a list of 128 lost its check and read past it (unsound; facts now come from Range::held_by_type, goals still from of_type). A let bounded only by i64’s own range no longer makes the solver refuse every later query in the function; bounds past ±2^62 are not stated as facts (#444’s limit).A number read through brackets past a jump is the number in any position
v[m[k] ?? panic(..)] and f(m[k] ?? return) lowered to an arm that handed back &i64, which rustc refused. The ?? now copies a number, bool or character read through brackets or get out itself where its fallback leaves, not only under a let.A grammar’s par_fold claims its pieces while the work runs (ADR-372 D4, built)
par_fold rule hands piece: and order: to the backend’s claimed pieces instead of cutting one piece per worker before the work starts.Bytes::par_pieces and par_fold options (ADR-372 D3-D8, built in part)
b.par_pieces(sep; piece:) is a Par[Bytes] of pieces that each end just after sep, claimed while the work runs (1 MiB by default, below 1 panics); at no the same pieces in order. A grammar’s par_fold takes ; order:, workers:, piece: (NK1102 for a wrong type, NK1109 for an unknown name) and runs on a pool of task::cores(); a failure is reported at its smallest position under either order. Pieces claimed while a par_fold runs follow with the winnow-grammar pin.bytes::find_all: the next hit costs a tzcnt and a cleared bit, one length test a block
benches/brc/scan.nika; Rust’s hand-written split, benches/brc/src/bin/scan.rs, is 31.3 / 29.4.bytes::ByteSet: a buffer searched for a set of bytes at the build’s width (ADR-375, built)
bytes::ByteSet(list) holds any bytes; b.find(set; from:), b.find_all(set; from:) -> Seq[i64] (its own count/nth/last, no position made for a count), b.split(set) by text’s rules; a set written empty is NK1287; a list of '…' handed to a Vec[u8] is bytes. The vector code is the new crate crates/unsafe/byte-blocks (AVX2 at v3, SSE2 at baseline, words elsewhere).Par::fold, par_fold and a grammar’s fold take an in-place step or merge (ADR-372 D9, built)
step and merge are each judged by their result type: returning the accumulator replaces it; returning nothing changes the first argument in place (a lambda is lent it; a declared function takes it mut or ref mut self; a merge may take its second argument as ref). Any other result type, or nothing without a mut first parameter, is NK1102. Holds for Par::fold, par_fold and a grammar’s fold.a ?? whose left is a view and whose fallback owns text is NK1185 again
?? whose left side is a view and whose fallback owns text (opt?.name ?? owned, s.strip_suffix(x) ?? owned) is refused with NK1185 again, as Part III says; #752 had lent the fallback as str.Par::fold’s step and merge return the accumulator or change it in place (ADR-372 D9, decided)
step and merge each either return the accumulator or return nothing and change their first argument (ref mut); the result type says which, the two choose independently, any other result type is NK1102. Holds for Par::fold, par_fold and a grammar’s fold (Part II 10.7, 12.6). Decided, not yet built (#717).task::cores(), Par::fold(init, step, merge; order:, workers:), par_iter(piece:) (built, ADR-372 D1, D2, D4-D7)
task::cores() is 1 at user_parallelism = no and the user pool’s size at yes (user-pool, 0 = the threads the process may run on) (#717).Par::fold folds with nothing shared: par::Order::Kept (default) merges contiguous runs in input order, Any keeps one accumulator per worker; workers: sizes the pool; at no it is one sequential fold and merge is not called.par_iter(piece: n) sets the piece size; workers: or piece: below 1 stops the program naming the option. A walk made from a Par is a Par, so fold is found after map and filter.A ref Bytes the body keeps takes the Bytes with its owner (built, ADR-373 D1, D4)
ref Bytes parameter of a function that keeps it (clones it, or hands it on to a parameter that keeps it), and that the result does not point into, lowers to &Bytes: its .clone() is a count, where it copied the bytes; a Bytes is borrowed, a mapping handed over at a count, and s.bytes, a cut or a &[u8] copied. One only read, or returned a view of, stays &[u8] (#718).A Bytes carries what owns its memory; Bytes(v) copies nothing; a mapping is a Bytes (built, ADR-373 D1-D3)
Bytes is a pointer, a length and a counted owner (memory it allocated, a Vec[u8] handed over, a mapped file); a clone is a count, equality, order and hash go by content (#718).Bytes(v) and fs::read hand the vector over without a copy; an fs::Mapped may be given wherever a Bytes is taken by value, at a count, and stays mapped while any holder lives.spec: a ByteSet holds any byte; a set proved empty is refused, one empty at run time finds nothing (ADR-375 D2)
bytes::AsciiSet becomes bytes::ByteSet, of any bytes 0x00 to 0xFF; NK1287 now refuses a set the build proves empty, and an empty set at run time finds nothing; split of an empty Bytes is one empty piece (ADR-375 D2, D3).spec: a Bytes searches for a set of ASCII bytes: AsciiSet, find, find_all, split; a sequence never loses its producer (ADR-375)
bytes::AsciiSet is a set of ASCII bytes built once (NK1287 for a literal byte at 0x80 or above); Bytes::find takes one, find_all gives a Seq[i64] of every position, split the pieces by text’s rules (ADR-375 D2, D3).std produces is the one the ledger names for it: find_all(set).count() counts per block and makes no position (D4).–cpu wins over the user’s RUSTFLAGS; a ?? fallback name of text and a text literal in a tuple of text lower (ADR-002 D6)
RUSTFLAGS or CARGO_ENCODED_RUSTFLAGS is set, it applies and --cpu comes after it and wins; before, Cargo dropped --cpu silently (spec; to build).names.first() ?? n with n a name that views text lowers, and a str view with an owned String fallback lends it (#752).(String, …) is wanted, each text literal is owned text whatever the other parts are, also in a list literal beside a tuple of text, which was NK1154 (#750).A T? beside an unsettled number is refused; a ref String name as a text view’s ?? fallback lowers (built)
opt == 5, 5 != opt, opt == -5 and opt == k with k typed by its uses are NK1102, as with a typed value (Part I 2.3); before, they passed the checker and failed in rustc (#748).(n.strip_suffix(s) ?? n).clone() with n a loop variable over ref Vec[String] compiles: the fallback is read as str; tools/prover_scope.nika is back to that form (#719).**codegen-units in [build.
[build.<target>] takes codegen-units, a whole number from 1, written into the generated profile only when set (ADR-002 D6, #692).--cpu=LEVEL takes x86-64 to x86-64-v4 on x86-64 targets, native everywhere and a target’s baseline; anything else fails naming the levels. It reaches every crate of the target, std included, through Cargo’s --config rustflags, and is a dimension of the compiled std’s cache key; native is keyed by the CPU found.**spec: codegen-units is a key of [build.
codegen-units joins [build.<target>]; absent, rustc chooses. ADR-002 D5’s “stays Cargo’s” is replaced: one unit against 16 moved 1BRC by about 42 instructions a row, in opposite directions for Nikaia and Rust.--cpu=LEVEL on build, run and test names the least CPU the program runs on (x86-64 to x86-64-v4, native); it is never a manifest key, is part of the compiled std’s cache key, and decides what ADR-371 D2 allows.process::run sets the child’s environment: env, env_remove, env_clear; the program is found on the caller’s PATH (built)
env_clear, then env_remove, then env; a later pair wins, a name both removed and set is set; names compare as the platform does (ADR-243 D5, #691).program with no path separator is looked up on the caller’s PATH (PATHEXT on Windows) and started by the full path found; a file there that is not executable is skipped, and none found is NotFound(program) (D6).= or NUL in a name, or NUL in a value starts nothing: one io::IoError::Other names every invalid entry, and a literal one is NK1286 (D7).vec![..].bits_as_ reads the same bits with the other sign; u8 gets every fixed-width name; S {} is a struct literal after throw (built)
u32::bits_as_i32, i32::bits_as_u32, u64::bits_as_i64, i64::bits_as_u64 keep every bit; u64::truncating_i64 and i64::truncating_u64 are gone, and a same-width truncating_ is NK1210 with help naming bits_as_; new u64::truncating_i32, f64::truncating_u32, f64::truncating_u64 (ADR-285 D42, #742).u8 has wrapping_, saturating_ and checked_ arithmetic, truncating_u8 out of every wider integer and f64, checked_u8 out of every wider integer (ADR-285 D41, #729; f64::checked_* still to build).throw S {}, return S {} and let x = S {} with empty braces are struct literals (#726).A map takes its types from what is written into it (built)
HashMap() or BTreeMap() takes its key and value types from the first insert, m[k] = v or entry(k).or_insert(v): a text key is String, number literals among the keys and among the values are one open number each, decided by their uses (D34); two types asked is NK1200, none asked is i32 (or i64), a map nothing writes into or types is NK1153. A key that is a view of text keeps the tier ADR-282 D13 decides (ADR-285 D43, #700).spec: process::run’s env, env_remove and env_clear; the program is found on the caller’s PATH (ADR-243 D5-D8, #691)
process::run takes env, env_remove and env_clear, applied as clear, remove, set; names are compared as the platform does; program is looked up on the caller’s PATH; every entry is checked before the start, a literal one while building (NK1286); env_clear is not a sandbox (ADR-243 D5-D8, #691).spec: a text position is a text::Pos; find gives a Match, a text is cut by before/after/between (ADR-374, #743)
text::Pos, a boundary in that one text, tracked as a view’s sources are. find and rfind give a text::Match (start, end); a text is cut by before, after, between and split_at, never with […]; a position moves only in a named unit (forward/back with scalars:, graphemes: or bytes:); it is kept in a text::Cursor, not a field. Part II: a grammar over text hands out text::Pos and text::Match. Part III: NK1214 reworded, NK1284, NK1285 (ADR-374, replaces ADR-320 D5, #743).A map takes its types from what is written into it
HashMap() filled with insert("a", 1), m["a"] = 1 or entry("a").or_insert(1) is a map from String to one open number, decided by its uses as a list’s numbers are (ADR-285 D43, Part I 4.5); a map nothing writes into or types is NK1153 (#700). Decided, not yet built.A lambda’s text item compares with text; a keep through a chain of calls; a literal beside a lent field
xs.iter().any(fn(w) { w == s.cfg }) and the like over a Vec[String] compile: a lambda’s lent text item is compared as str (#688). A parameter kept in another argument’s place through two or more calls ties the same lifetime, and a local handed down such a chain into a longer-lived list is handed over (ADR-283 D27, ADR-282 D25, #740). A text literal beside a field read through a ref parameter is a view of static text, so the result lends (ADR-282 D13, #740).mut p: ref T is mut p: T; a list into Vec[T?] wraps its elements
ref in a mut parameter’s type adds nothing (Part I 6.5): it lowers to &mut T, the plain call is accepted, and put(ref t) is NK1137. [f"{1}"] where Vec[String?] is wanted is vec![Some(..)]. An untyped comptime written out at its uses no longer draws rustc’s “never used” (#700).A view read through ?? is watched; a ref Bytes is a run of bytes
let f = m[k] ?? return and f.result ?? return are views into the map’s owner: handing it on as mut, or changing the part they read, while they are still used is NK2305 (#739); a change to another field stays allowed. ref Bytes lowers to &[u8], so f(s.bytes) and HashMap[ref Bytes, V] build (#700).spec: the same bits with the other sign are bits_as_; truncating_ only narrows (ADR-285 D42, #742)
bits_as_i32, bits_as_u32, bits_as_i64 and bits_as_u64 read a same-width integer’s bits with the other sign. truncating_T converts only out of a wider integer of either sign and out of an f64; u64::truncating_i64 and i64::truncating_u64 are replaced (ADR-285 D42, #742).An element or field read through a borrowed parameter is lent into the result; a local kept in a longer-lived list is handed over
v[0] from a ref list is a view (ADR-282 D13). A field read through a ref parameter is lent into a mixed result in the tail form and the return form alike (D13, D15); a published mixed result refuses either with NK1131 (D22). A field tail of a ref parameter no longer reports a false NK1104. A local that a callee keeps in a list living longer than the local is handed over, and a later read of it is NK2105 (D7, D25; #738).A String? parameter handed back is lent; mixed text into a published position is refused; an unlinked list keeps one type
String? parameter returned as it is, or as a side of ??, is a view like its ref String? twin (#696, #678). A mixed String? unwrapped by ?? makes the list, field or set it goes into mixed; into a published position, the view behind it is NK1104 (#696). A std-built or untyped list handed to a container parameter keeps Vec<String>; a view kept there is NK1102 (#738).Lowering: text tiers, the solver and result sources redo only what changed (#727)
A float beyond f32 is NK1116 in every f32 place; a mixed String? result lowers
let w: f32 = 1e40, and the same literal as an argument, return, field, list, assignment or operand beside an f32, is NK1116, as as f32 already was (#737). A function whose String? result mixes views and owned text compiles: a match/if whose arms all leave is not wrapped in either, and a String? arm goes into either_maybe without a second Some (#696).f32 is a whole type: as f32, comparisons and arithmetic
as f32 is accepted wherever as f64 is (no more NK1122), and an f32 compares with == (no more NK1188). A float written out beside an f32 is one. A float written out beyond the f32 range in as f32 is NK1116, and a run-time f64 as f32 aborts only when a finite value leaves the range. A float compared with another number type is NK1199. f64 as u8/u32/u64 compiles (#736).A function’s contract is found as a view; owned fields keep no view (#727, #735)
A result’s sources are what it borrows from; a field behind a view is lent (#735)
A mut argument says mut, and a float literal is an f32 where one is asked
mut parameter is changed: a let without mut is NK1139, a parameter NK1138. A for binding cannot be changed (NK1139). A match arm may change what it binds. A float literal takes f32 from its place: let, argument, return, field, list, assignment. A list mixing a float and an integer is NK1154 alone (#734, #728).Text orders against text; the prover answers repeats without normalising (#733, #727)
a < owned) compiles; it lowered to &str < String. The prover answers a question asked again under fresh term ids without normalising it again.Five more programs the checker accepted now compile or are refused
mut parameter copies out and then changes is the arm’s own let mut copy. A let of a call that hands back a TaskHandle is NK2109 when nobody takes it. A stored fn(ref A, ref B) -> ref R sync gives its struct no view. A pattern of a struct whose every field is a binding, _ or .. covers every value without else (Part I 3.4, NK1151). A with naming every field reads its base as _ (#730).Lowering: the solver and hot lookups stop copying (#727)
Four programs the checker accepted now compile, and a fifth is refused
x ?? 1 on an f64? (and a float beside an integer) is NK1103. A struct pattern that leaves a field out without .. is NK1234, at any depth. with keeps a function put into a field of code. A catch on a call through a parameter of code opens the box by type, and a function that cannot fail can be handed to a fn(..) throws parameter. A trait method returning a literal keeps the trait’s result type (#732).Every program a checker test accepts is checked by rustc
NIKAIA_DUMP_ACCEPTED makes the checker write each accepted one-file program, and CI’s accepted-programs sweep lowers them all and checks them with rustc; a refusal not on its allow-list fails the build (#705).Lowering: the solver copies less and the ledger pass skips report-only walks (#727)
A pattern of another type is a refusal; a fold’s value start and a text match compile
match 5 { "a" => … }, and a variant of another enum, are NK1103. A grammar fold started from a value such as Tally { … } or 0 is lowered as a function that makes that value, and a step that assigns to the accumulator’s fields changes it in place. An owned String matched against text literals compiles (#704).Lowering: the solver, proofs and diagnostics stop repeating work (#727)
Four more checker gaps from the rustc sweep are refusals
NK1283 (let, for, nested). A struct or enum of the program named as a value without a constructor is NK1230. A body with a declared result ending in an if without else is NK1104. s ?? 5 on a String? is NK1103 (#704).Four checker gaps from the rustc sweep are refusals
NK1104; a catch block whose replacement value is of another type than the value it guards is NK1103; a literal matched on a T? with no null arm is NK1205; a SharedMut[T] or Locked[T] in an f"…" hole is NK1201 (#704).An impl narrower than its trait’s declaration is the type’s own method and a bridge
sync or non-throwing method (D2). That method is now lowered into an inherent impl (pub when the type is), so a call on the concrete type follows its own contract without .await or ?. The trait’s impl is a bridge with the declaration’s signature that calls it, awaiting it and wrapping it in Ok as needed (#703 item 11).A binding may shadow a comptime; a part of a mut parameter is handed on in place
let, for or lambda binding named like a comptime is written under its own name below, and the constant keeps its name. A part a match over a mut parameter binds goes on to a mut parameter as &mut *part. A member of a lambda parameter whose value has no known type is NK1245 (#703 items 7, 9, 10).Lowering runs each analysis once (#727)
spec: u8 has every wrapping, saturating, checked and truncating name (ADR-285 D41, #729)
u8 gets wrapping_, saturating_ and checked_ arithmetic as u32 has them, and truncating_u8 / checked_u8 out of a wider integer and an f64 (Part I 2.2; ADR-285 D41, ADR-315 D1, D4).A word is never a view inside a type either; TaskHandle and two-view function types lower
ref on a number, bool, scalar or bare enum is NK1257 inside a type too (Vec[ref i64], (String, ref u8), fn(ref i64) -> i64). A written TaskHandle[T] lowers to std’s handle. fn(ref String, ref String) -> ref String gets one lifetime for the call (#703 items 5, 6, 8).Lowering looks methods up by name (#727)
An integer literal is no float; a lambda takes what it names
let x: f64 = 2, half(3), x * 2 and [1.5, 2] are refused (NK1103/1102/1104/1105/1106/1199/1154) instead of reaching rustc, as Part I 2.4 decides. xs.map fn { 7 } is NK1101 (Part I 5.3). An unused String? word of main compiles. A function with its own error channel passed as a fn() throws has its error put in the box (#703 items 1-4).spec: a Bytes carries what owns its memory; a ref Bytes only read takes any run of bytes (ADR-373, #718)
Bytes holds its own allocation, a Vec[u8] handed over with Bytes(v) (no copy), or a mapping; a fs::Mapped may be given wherever a Bytes is taken, read or kept. A ref Bytes parameter that is only read takes any run of bytes (Part I 2.6, Part III 17.2; ADR-373, ADR-283 D20 changed).view changes: methods are looked up by name, not by a scan of every function
a caught method call adds nothing to the caller’s error channel (#725)
catch that keeps its error adds to the enclosing function’s inferred error set only the fatal errors the handler does not name, as a caught function call already did. A function declared throws that throws nothing it can name passes its unnamed error (?) on to its callers, which widen to the boxed channel (ADR-280 D9) instead of keeping one named type and failing in rustc.check: an open number is handed on once; a + a + … + a checks in linear time
open_numbers_in handed back a number once for each place it stood in a value, so every operation of a + a + … + a asked, joined and marked each copy and cloned the number’s entry each time: 500 terms took about six minutes. It now hands each number back once; what is asked and decided does not change.The user cache is bounded by size, least recently used first (#724)
~/.cache/nikaia is held to NIKAIA_CACHE_LIMIT (default 8 GiB, 0 = no limit). After a build adds an entry, the entries used longest ago are removed until the cache fits, however young they are; an entry a running build holds is never removed. Last use is a stamp nikaia writes, and in-use is a shared flock or Cargo’s lock. This replaces the eight-per-kind and 24-hour rules. New nikaia cache prune runs the pass and prints what it removed (Part III, ADR-021 D12.5).a parameter handed to a position the callee keeps is kept, even where the callee’s result could point into it (#720)
longest(s, "ab") where a literal can also come back, keeps that parameter instead of lending it; before, this was refused with NK1102, or NK2302 when the call was returned. What each result points into is worked out again until keeps settles, and text moved whole into a result of either kind is no longer read as a view of the argument.text tiers: a tie is held once; a place that reads itself no longer doubles its ties (#702)
out.push(f(out))) they doubled with every such statement, and checking crates/rs2nika/convert/src/main.nika ran out of memory. A tie already held is no longer added again; what is lent does not change.A program’s std no longer compiles the compiler (#723)
src/tools, nikaia_std::tools) is compiled only under the new compiler feature, which only crates/nikaia enables. std’s own run-time Nikaia, the HTTP/1 head grammar, moved to src/runtime, a package of its own lowered into src/runtime/package.rs (nikaia_std::runtime). Building a program’s std: about 120 s to 10 s of CPU, rlib 37 MB to 4.8 MB. A change to a tools/*.nika no longer rebuilds programs’ std.a catch also catches a failing call inside the expression it guards (#721)
g(f()) catch { … } and read(m.text()) catch { … } failed in rustc because the inner call was left as a Result. The guarded expression is now a labelled block that each failing call in it leaves with its error, and the handler gets every error that arrives there (Part I 7.1).A path of fields is a name of the bounds walk and the prover (ADR-306 D17, #711)
t.slots, p.end and self.at as names, and path.len() as a length. Through a borrowed parameter the facts hold for the whole body; otherwise they are dropped at a write to the path or a path it is part of, at a call handed the root to change, at a method that changes its receiver, and in a lambda, task, overlap branch or select arm that changes it.assert(p.start >= 0 && p.end <= b.len()) is a precondition its callers prove; a struct literal’s fields are known.Array[T, N] has len() == N.a name read later, or a parameter arm, goes into a keeping parameter as a view (#716)
String parameter is lent instead of moved when it is still read after the call, or in a later turn of a loop around the call; the parameter then takes both kinds (EitherText). An if/match arm that is a parameter, in a choice handed to a keeping parameter, is a view likewise. A name nothing reads afterwards still moves; a pub fn’s parameter is unchanged.fs::map maps bytes; m.text() checks the mapping once (ADR-016 D5, #710)
fs::map checks nothing and Mapped derefs to Bytes. New fs::Mapped::text: the first call checks the whole mapping in parallel and throws io::IoError::NotText at the earliest bad byte, later calls return the same view. A cut’s text() checks only the cut. A grammar run over a mapping checks its text once and reports a byte that is not UTF-8 as a parse error at its line and column.spec: parallel folding without a grammar (ADR-372, #717)
task::cores(), Par::fold(init, step, merge), b.par_pieces(sep); par_fold and Par::fold claim pieces while they run and keep input order unless order: par::Order::Any gives it up; workers: and piece: options (Part II 10.7, 12.6; ADR-372, ADR-296 D11 changed)an arm that is a name still read later is a view of it (#712, #708)
if or match arm that is a name of text of its own, still read after the choice or in a later turn of a loop, is a view of it: beside an arm that makes text of its own the choice is EitherText, and the name is lent, not moved (ADR-282 D7, D13). Before, nikaia check accepted such programs and rustc refused them with “borrow of moved value”.Vector lanes, crc32c and prefetch are portable std; an architecture’s instruction is a safe function (ADR-371)
std offers a portable vector of lanes loaded from a Bytes (checked and proved as the cut), masks and their first lane, crc32c and a prefetch hint, each meaningful on every target; an instruction of one architecture is a safe function of its backend module (std::backend::x86::pext), callable only in a build for a CPU level that has it; assembly stays the last way out (ADR-371, Part III 16).@inline asks, the compiler adds the hint where it knows better (ADR-370)
@inline, @inline(always) and @inline(never) before a function or method; a hint that never changes meaning and lets another package inline a pub fn; the compiler adds the hint itself only in obvious, measured cases where the backend leaves a hot call out of line (ADR-296 D41 the first), and a written @inline(never) wins (ADR-370, Part I 5.6).a view of a parameter kept in another parameter’s place points into the caller’s argument (ADR-283 D27, #706)
mut parameter’s list or field compiles: the two share one lifetime in the lowering, an argument made in the call lives in the caller’s keep, and changing a named argument in place while the place is read is NK2305. Free functions only so far.A Bytes reads a word: u32_le, u64_le, u32_be, u64_be (#709)
b.u32_le(i) … b.u64_be(i) on Bytes and s.bytes read 4 or 8 bytes in the named order, checked as the cut and stopping with its message; under 0 <= i && i + n <= b.len() the read is proved and lowers to one load through proven::array. A signed i64 is the u64 read through truncating_i64(). In the hand-written 1BRC steps it cuts 9-20 % of the instructions a row.A type’s invariant: an assert after a struct’s fields, and found where none is written (ADR-369)
assert(claim; message: …) after a struct’s fields is the type’s invariant; it is proved or checked at every construction, at the end of every method that changes the value and after every outside write, and is a fact wherever a value of the type is read; where a struct with private fields writes none, the compiler may find one its package always keeps and uses it only as a fact (ADR-369, Part I 4.1).A field is a fact of the prover while nothing can change it (ADR-306 D17, #384)
t.slots, p.end, self.at) is a name of the bounds and overflow walk; a fact about it holds for the whole body through a parameter the function only borrows, and otherwise until a write to it, a call that may change its root, or a lambda or task that sees the root; a precondition (ADR-269 D5) may read a field of a parameter (ADR-306 D17, ADR-269 D9, Part III 14.2).programs without ref no longer refused for the text tiers’ choice in three shapes (#708)
if/match hands back each arm as a return does; a kept parameter handed back whole into a result of either kind is no NK2302; a parameter lowered as a view whose result keeps it no longer counts as handed over (NK2305).A field is a fact of the prover while nothing can change it (ADR-306 D17, #384)
t.slots, p.end, self.at) is a name of the bounds and overflow walk; a fact about it holds for the whole body through a parameter the function only borrows, and otherwise until a write to it, a call that may change its root, or a lambda or task that sees the root; a precondition (ADR-269 D5) may read a field of a parameter (ADR-306 D17, ADR-269 D9, Part III 14.2).a view of a parameter kept in another parameter’s place points into the caller’s argument (ADR-283 D27, #706)
fn words(s: String, mut v: Vec[String]) pushing parts of s into v gives the two parameters one lifetime, and the caller’s argument lives as long as the place’s owner (Part I 6.6).fs::map hands back bytes; m.text() checks them once (ADR-016 D5)
fs::map checks nothing and Mapped derefs to Bytes; m.text() checks UTF-8 over the whole mapping the first time, throws io::IoError::NotText with the earliest bad byte, and hands back the same view after; a cut m[a..<b].text() checks only that cut; a grammar run over a mapping checks the text it reads (ADR-016 D5, Part III 17.2). ADR-283 D22 is superseded.A Bytes reads a word in the order its name says (ADR-320 D12)
b.u32_le(i), b.u64_le(i), b.u32_be(i) and b.u64_be(i) read 4 or 8 bytes from i as an unsigned number, checked as b[i..<i + n] is and proved as it is (ADR-320 D12, Part I 2.6). ADR-296 D12 says it rejects a scanning construct of the language, not a program reading words.Bytes::find searches with memchr; range indexes inline (#701)
Bytes::find and find on a run of bytes search with memchr, word-wide, as ADR-320 D6 promises, not one byte at a time; indexing with a range or a kept Span is inlined like a scalar index.drain takes its container away (#707)
for x in c.drain() is NK2105, as the ledger’s self: Vec[T] says; before, the checker accepted it and rustc refused “borrow of moved value”. A container that stays the caller’s (a mut parameter, a field under ref mut self) is emptied: a list with drain(..), a map with drain().a chain of map reads past a jump copies numbers out; a method’s result ties the text handed to it (#694, #702)
a[k] ?? b[k] ?? return x over maps of numbers copies the number out at each link (#694). A program method’s result that views a String argument, stored where it outlives the caller’s text, owns that text, as a function’s result already did (#702, ADR-282 D12).A Bytes read and i + k under a bound are proved (#698, #699)
Bytes read and cut are proved like a list’s: b[i] and b[i..<j] are bounds candidates, a Bytes len() is a counted length (ADR-306 D12), and a proved cut lowers to proven::cut.i + k under i < len is proved: with a literal k >= 0, x + k asks only the upper bound and x - k only the lower (ADR-306 D6); an untyped counter (let mut i = 0) is an arithmetic site once its type is known.A struct viewing a kept String parameter, stored where it outlives it, owns its text (#702)
self or an outer scope’s local that outlives the caller’s text; the position then takes what its callers hand it (ADR-282 D12, D13). Before, rustc refused such programs (v does not live long enough).A struct of views read through a local does not keep the argument it views (#697)
returns = "...: ref(s)") keeps that argument only where the result leaves: returned, stored in a field or container, or handed to a keeping parameter. fn shout(v: String) { let a = tag(v); println(a.label) } now lends v, and the caller may use its text again (ADR-094 D2).A struct handed back through a let, and a parameter handed to a reader, lend their String parameter (#678)
fn tag(s: String) -> Tag { let t = Tag { label: s }; return t } and fn f(a: String) -> String { g(a); return a }, where g only reads its argument, lower byte for byte like their ref String twins (ADR-282 D2, D5, D12; Part I 6.5). The ref String twin of the first shape is no longer refused with NK2302.split_once parts past ?? continue are views (#687)
continue and break after ?? as no value, as it did return, so the names let (a, b) = w.split_once(..) ?? continue binds are views of w, and a list or map they go into becomes one of views. A bare tuple pattern (a, b) in a match on split_once binds its parts the same way.An idle task pool at yes sleeps (#693)
Pool::idle rearms and wakes only after a park that saw something move, the pilot no longer rings the bell itself, a sleep on a pool task is woken by its time, and a second parker waits on the bell while another thread holds the I/O ring (park_in_ring). On 1BRC at yes, voluntary context switches drop from about 10 000 to about 70, and sys time from about 0.2 s to 0.owned text assigned whole is moved; a map read past a jump is a view (#686, #694)
l.from = key) is moved, as it already was in a struct literal, a let and push; it is no longer put into the keep frame, which rustc refused. A ?? fallback that reads a map and jumps when the key is missing (a[k] ?? b[k] ?? return x, also continue, break) counts as a view of the map’s value, as b[k] alone does; NK1185 still refuses a fallback that is a value of its own.four defects that kept develop red, from text tiers and and_modify (#678, #680, #684)
and_modify on a map of numbers lowers again: its body runs as an awaited async block between the copy out and the write back. A variant’s static-text payload bound in a match arm keeps text of its own where the arm hands it on beside text of its own (ADR-282 D29). A String parameter that a caller hands a choice of a view and text of its own is not lent (ADR-282 D12). An argument made in a call to another package’s function returning a struct of views lives as long as the result (ADR-283 D24).process::run’s max_rss_kib on Linux counts the caller’s high-water mark (ADR-368 D3)
process::run’s max_rss_kib is the kernel’s figure as it stands: on Linux it is never below the most memory the caller had held resident before it started the program, since the kernel counts the image exec replaced. ADR-368 D3 and Part III say so; the figure itself is unchanged.A map’s text keys and values are views where only views go in (#659)
HashMap[String, V] key or value is decided by what flows into it: views make it a view, only literals make it static text, text of its own makes it String, both make it EitherText. A loop over a map binds its key and value separately, each with the kind of its own position. Any text can look up a map keyed by views. examples/1brc.nika, access-log and k-nucleotide drop ref from their map keys and lower byte for byte as before.Four lowering defects found by the 1BRC programs (#683, #684, #685, #676)
?? that panics keeps find’s position: find(..) ?? panic(..) cuts its text as ?? return does (NK1214).and_modify on a map of numbers: fn(v) { v += 1 } and fn(v) { v = v * 2 } change the number in the slot.entry builds its String key: a literal or a view handed to entry on a map keyed by String is converted as insert’s key is.for i in 0..<n no longer breaks the first loop’s xs[i].a program reads the clock, and what a program it ran cost (ADR-368)
time::now() reads a monotonic clock as a time::Instant (elapsed(), b.since(a), zero where b is earlier); d.in_nanos() joins ADR-150’s readers; process::run’s Output reports elapsed, user_time, system_time and max_rss_kib (i64?) for that one child, through wait4 in the new crates/unsafe/child-usage.A String parameter changed through a name bound to it owns its text (#681)
String parameter changed in place through a name bound to it (let mut s = who; s.push_str("!")), as a place, or as a list element owns its text and is never mixed (ADR-282 D29). A view handed to it is refused at the call with NK1102, published or not; before, nikaia check took it as EitherText and rustc refused the push_str.A variant’s String payload is decided by what flows into it (#680)
String payload is decided like a field (ADR-282 D2, D12): only views flowing in make it a view and the enum takes the lifetime; text of its own keeps it String; both make it EitherText; only literals make it &'static str. A pub enum may become a view but never mixed. NK1261 also warns on a payload written ref String. examples/json writes Text(String) and lowers as before. Fixed on the way: a guard reading a view bound inside a box compiles.a program can measure time and what another program used (ADR-368)
time::now() reads a monotonic clock as a time::Instant (elapsed(), since); a Duration reads as as_nanos() (i64, saturated) and as_secs_f64(); process::run’s Output reports elapsed, user_time, system_time and max_rss_kib (i64?) for that one child. Decided, not yet built.A String parameter handed back unchanged is lent (#678)
String parameter handed back as it came in is lent, and the result or the field of a struct literal handed back that it goes into is a view of it (ADR-282 D2, D5, D12; Part I 6.5): such code lowers as its ref String twin and the caller may use its argument again (was NK2105). It keeps its text where text of its own also goes there, where the field is published or filled elsewhere, and where the parameter is mut, String?, a trait method’s, reassigned or changed in place. Also fixed: a call handing back views of an argument made in the call no longer drops that temporary (E0716), and two calls of one function in one statement no longer share an argument’s keep. Part I 6.5’s Pair, pair and longest write String.The specification’s examples write String where ref said nothing more (#654)
label, and Part III’s html escape and row, drop a ref the program means the same without (ADR-282 D2, NK1261). Pair/pair and longest keep it: without it the parameters are kept today, not lent.ref String outside the C boundary is a warning (NK1261, #654)
ref on text written on a parameter, an option, a field, a result or a let says what String says (ADR-282 D2). Outside extern "C" and pub extern fn it is now the warning NK1261, and the program means the same without it. Text the compiler made a view where the source wrote plain String gets no warning (the tier pass records those positions, Parsed::text_views).a fold’s counters proved from the input’s length
fold or par_fold over a grammar’s input, a whole-number accumulator field that each step changes by a fixed literal amount outside any loop, and that the merge sums, is proved not to overflow where k·2^60/max(w,1) fits its type (w the element rule’s least width), and is written without its check under remove-overflow-checks:on. The proof starts only from an input whose length is known (text, Bytes); a streaming input is unbounded (D2). 1BRC’s count loses both checks; sum keeps its checks until D4. --bounds lists x.f += d and a fold’s lambdas.A list repeat keeps what it repeats (#654)
[x; n] counts as keeping x (ADR-282 D6), as [x] does, so a String parameter repeated into a list is no longer lent, and [name; 3] is NK1280 again rather than NK1104.a fold’s counters proved from the input’s length (ADR-367)
fold or par_fold over a grammar’s input takes at most len / max(w, 1) steps (w the element rule’s least width); a whole-number field of the accumulator that each step changes by at most k, with an additive merge, is within ±k·steps: its check is proved where k·2^60/max(w,1) fits, else checked once against the input’s length before the fold. Only for an input whose length is known at the start (text, Bytes), never over a stream. Decided, not yet built.A shift written only in literals is folded exactly (#661)
1 << 40 is 1099511627776, an i64, and 1 << 31 is 2147483648: a shift written only in literals takes the first type that holds its value, as + and * do (ADR-285 D22, D40). 1 << 63 is NK1116 from Nikaia, no longer rustc’s arithmetic_overflow. An open let given such a shift is typed by its value. Where a use asks for a type the shift is that type’s: let m: i32 = 1 << 31 stays -2147483648, let m: i32 = 1 << 40 is NK1116, and an i32 field given 1 << 31 no longer lowers as 1i64 << 31i64.A String parameter or option the body only reads is handed on as a view (#654)
String parameter or a lent String option that the body only reads is checked as the view it is below. Handing it on to another call or to std lends it as it is (count(dna, k), net::listen(at)), with no & that a ref String parameter does not get, so dropping ref from such parameters lowers to the same Rust. Method parameters still take their argument (ADR-288).A shift written only in literals is folded exactly (#661)
1 << 40 with nothing asking is the value 1099511627776 and takes the first type that holds it, an i64, as + and * do (D22); 1 << 31 is 2147483648; 1 << 63 is refused. Where a use asks for a type, the shift is that type’s and loses the bits it moves out. Specification only; the compiler follows (#661).a grammar run takes in a small rule it calls
std’s map hash spreads integer keys
FxHasher::finish rotates left by 26 as rustc-hash 2 does, so integer keys on a power-of-two stride no longer crowd into a few buckets (100 k keys i*1024: 699.7 to 71.3 instructions per operation; 1BRC +1.5 a row).A cached build says its warnings again
nikaia run, build and lower reused a cached lowering without saying the check’s warnings, so a warning appeared only on the first build of an unchanged source. The cache now keeps the warnings with the lowering and a hit says them again; older cache entries miss once.Static views of text: a published list and build-time values own their text; winnow-grammar 3c50bc4 (#659, #453)
pub fn f() -> Vec[String] { return g() } stays Vec<String>; option defaults, comptime values and field defaults read what they read as text of their own.String? parameter only literals reach stays Option<&'static str>, a call returning static text keeps no argument alive, and text => over a nullable view binds the payload as it is.String for a field declared String, whatever it lowers to.Static views of text: a parameter only read is &str again (ADR-282 D5, D29, #659)
String parameter the body only reads is &str again, also when only literals reach it, and so is one that only hands it on to such parameters.["a", null], a list of text handed whole to a std function, a let map stored in a library field; methods in a trait impl count as published.text => … arm over a ref T? binds the reference, as it should; it no longer writes let text = *text.A map is hashbrown’s table (#420, #434)
HashMap and TrustedMap lower to hashbrown::HashMap with the keyed and the Fx hasher, and a lookup no longer reserves a slot: 1BRC from 300.1 to 273.3 instructions a row; messages still name HashMap<K, V>.rfind cuts its text (ADR-320 D5, #453)
t.rfind(…) hands out a position of t as find does, and t["lit".len()..] (the start moved by a literal’s byte length) cuts any text, as t[0 + "lit".len()..] already did (Part I 2.6, NK1214).lower-std: five times faster, and seconds when nothing changed (#667)
lower-std runs the package’s ledger pass once, lowers the tools files on every core with unchanged output, and caches its result in the user cache, keyed on the compiler and every source read (242 s to 40 s uncached, 0.15 s unchanged, on 4 cores).A text is cut by a position it handed out (ADR-320 D5, #453)
find’s result, either end of the text, and such a position moved by a text literal’s byte length cut a text without a view; any other number is NK1214, naming s.bytes[a..<b].text() and s.graphemes.take(n).for over a Bytes the loop variable is a u8, so c == '
' compares bytes.A position only literals flow into is a view of static text (#659)
if/match), a let, a parameter or a list element into which only literals flow lowers to &'static str, with no String::from and no lifetime (ADR-282 D29); with text of its own beside them the literals are built as before. An empty local Vec() handed whole into a field takes the field’s type and tier, so pushing literals into it no longer gives NK1106. Published positions and map keys and values still hold text of their own for now. s.bytes handed back from such a parameter is copied (ADR-320 D11).a ‘…’ is typed by its uses, also through a let (ADR-320 D8, #453)
'\xNN' above \x7F is a u8, and NK1212 where a scalar is wanted; 'é' where a u8 is wanted is NK1212, with "é".bytes in the help.let c = ';' is a u8 where its uses ask one (b.find(c)), as a number’s let is typed by its uses; uses that ask both a u8 and a scalar are NK1200.s.normalized(text::Form::Nfc); a ‘…’ of several scalars (ADR-320 D8, D10, #453)
s.normalized(text::Form::Nfc) (also Nfd, Nfkc, Nfkd) is std’s normalisation (Unicode 17.0.0, unicode-normalization 0.1.25); a form is a value that can be held and passed on.'…' of several scalars is the one NFC composes them to, with the warning NK1213; otherwise it is refused with NK1212, whose help gives the text literal.\u{…} naming a surrogate is NK1184, in every literal; Part I 2.5 and Part III say so, and NK1212 no longer lists it.s.scalars and s.graphemes walk a text (ADR-320 D3, D4, D9, #453)
s.scalars is the text’s scalars and s.graphemes is what a reader sees as one character each, by UAX #29 in Unicode 17.0.0 (unicode-segmentation 1.13.3); both are read with take, skip, count and for, and a grapheme compares with a text literal.s.bytes kept beyond the text is copied wherever it goes; Bytes has clone (ADR-320 D11, #453)
s.bytes bound by a let, pushed, stored in a field (also as P { b }) or returned is copied into a Bytes of its own where it is kept, and stays a view everywhere else.Bytes::clone() is a copy of its own, also of s.bytes and of a read-only Bytes parameter.s.bytes is a Bytes, borrowed (ADR-320 D11, #453)
s.bytes reads the text’s own bytes, borrowed, with every read a Bytes has; a read-only Bytes parameter is a &[u8], so fs::read bytes and s.bytes go to the same function; kept in a field or handed back, it is copied.s.bytes is a Bytes; a normal form is an argument; any over bytes
s.bytes is a Bytes over the text’s own buffer, a view wherever a view suffices (ADR-320 D11, #453).s.normalized(text::Form::Nfc); the form is an argument with its path, not an option (ADR-320 D10, #453).Bytes, a lone any is a checked scalar; inside bytes(...) it is the raw byte (ADR-366 D2, #422).A grammar reads Bytes (ADR-366)
String or a Bytes (from fs::read); a grammar called with both is compiled for both (#422).until, a literal’s capture and a run of any are checked text; a byte that is not UTF-8 throws ParseError invalid UTF-8 at byte N: 0xXX, N from the start of the whole input, in par_fold pieces too.bytes(p) hands back the bytes p covered, unchecked, over either input.Bytes is refused; a Mapped counts as text.A String result that hands back a view of ref self is a view result (#663)
-> String returning a field of ref self (or of a ref T parameter), or a method’s view of it, is decided a view and lowers as -> ref String; NK1131/NK1104 no longer fire there. A result that also gets text of its own stays String and asks for .clone(), pub or not (ADR-282 D2, D12, D22). A std method’s view result now counts only for a receiver of that std type.A grammar reads Bytes (ADR-366); NK1116 once, a caret under a number, a tuple let over a lent item (#662, #602, #626)
Bytes as well as a String, the argument decides; over Bytes, what becomes text or a scalar is checked as UTF-8 where it is made, a bad byte is ParseError not UTF-8 at byte N, and bytes(p) yields the bytes it covered (Part II 10.8, ADR-366, #422). Specification only: the compiler follows in #422 after winnow-grammar#24.let whose constant value overflows is refused with NK1116 where it is written; the name no longer carries that value forward, so its uses are not refused a second time (#662).fn(pair) { let (name, v) = pair … }, the form NK1101’s help recommends, reached rustc as v > 1 over a &i64. A walk now records whether it lends its items (Vec::iter, and filter, take and the like over one that does); a tuple taken apart from such an item reads a number, bool or char as the value and keeps text a view, as a for head does (#626).bool bound in a nested match on a reference no longer reproduces as &bool; a test keeps it so (#578).An option is lent as a parameter is
String option the body only reads lowers as one written ref String does: &str in the declaration, its default and a literal passed as written, an owned value lent with &. Options join the keeps analysis, and a parameter handed to a kept option is kept. An option is listed in the ledger’s keeps only when kept. An owned String passed to a ref String option is lent instead of refused (NK1106, #455) (#654).An overflow rustc would refuse is NK1116 first: shifts of open numbers, a followed let mut, <<= and -x
i32 where nothing asks (Part I 2.4), so let n = 1; n << 40 is NK1116 rather than rustc’s arithmetic_overflow. A let mut’s value is followed through the code of its own block: it is forgotten at an assignment inside a branch, loop or lambda, and not seen inside a loop or lambda that began after it. So let mut m = 1000000000; m = m + 1; println(m * 3) is NK1116, and a divisor assigned 0 is NK1118. x <<= k is checked like x << k, and -x over the smallest value of a signed constant is NK1116 (#656).A position only literals flow into holds a view of static text
let, a list element or a map key or value into which only literals (and other views) flow now holds a view of the static text instead of a constructed copy; the literal is constructed only where text of its own also flows into the position (Part I 2.2, 2.3; ADR-282 D29, #659). Specification only: the compiler follows in #659.A tail if/match with a return end puts only its values into Some
T?, a tail if or match whose ends mix values with an end that jumps (return, break, continue, throw, or a block ending in one) now puts each value end into Some and leaves the jumping ends as written. Before, one jumping end left every end unwrapped, and the generated Rust was refused. The same holds beside a null arm.A String that only views reach is a view at grammar rules, parsed parameters, std views and map keys
ref dropped, a rule’s result, a field filled by an action, a parameter handed to a parse, a std view method’s result and a map key filled by entry or split_at now lower as they did with ref String, instead of NK1104/NK1106/NK2302 or a copy. A String key no longer reads as kept by std’s get(self), so the generated Rust compiles. The ledger lists lent String parameters as borrowed. NK1257 now also refuses ref int.A tail if/match takes the wanted T?; a shift past the width is NK1116
(A, B)? or T? now reaches every end of an if or match used as a body’s tail: tuples of text literals are built as String, a null end is allowed, and the other ends are put into Some. A shift by a constant count below 0, or as large as the left side’s type is wide or larger, is refused with NK1116 before the backend (Part III).A throw of a call, a map’s read past a jump, a variant fallback and a kept pausing function lower as written
throw made() throws the type made returns. In std and the tools package a library error is written by its full nikaia_std:: path. An untyped let x = m[k] ?? return binds a word by value, as a typed one does. A map’s read past a jump goes to a ref parameter without another &. A variant fallback beside a borrowed option of a word is by value. A kept fn(ref P) -> T that may pause borrows P for the call.An assert on an option is a precondition
;) is a precondition as one about a subject is: a call proves it with the value it names or the option’s default; for main it is checked where the words arrive (exit 2) and listed in --help, --asserts and nikaia.contracts. An option’s type is checked as a parameter’s (i8, i16, u16, an undeclared type: NK1135). The usage error says a type with the article its sound takes (a u8, an i64).A match on a lent name, and an indexed write on an Array, build
match on a name an enclosing arm lent, taken out of a box or not, lowers to a match on what the name points to; rustc refused it before (cannot explicitly borrow within an implicitly-borrowing pattern), or bound a number by reference. xs[i] = v on an Array[T, N] builds.split_once and rsplit_once on String
text.split_once(sep) and rsplit_once(sep) cut the text at the first or last separator and return both halves as views of it, (ref String, ref String)?, or null where the separator does not occur (ADR-364). A plain value as the tail of a function declared -> T? is wrapped in Some, as a return value is; rustc refused it before. The converter writes Option<(A, B)> as (A, B)? and keeps a let’s Option<T> type whole.nikaia run compiles a program that needs only std without Cargo
rustc directly against std built once as a dylib (ADR-321 D3/D4), at opt-level 0 without debug info; overflow checks stay on in the program, and the panic strategy is the same. It is rebuilt only when the lowered Rust, the toolchain or the std library changes. Output, abort text and exit codes match the Cargo build, and rustc’s messages still come out in Nikaia’s words. nikaia build, other targets, dependencies and libraries still go through Cargo.(A, B)? is a type
? in every type position, as every T? does, and lowers to Option<(A, B)> (ADR-364, Part I 2.3). p?.0 is the first part as a nullable, a further ?. step reaches on (ADR-278 D10, D14), and p ?? ("", "") falls back to one value, the tuple. NK1254 is retired.a build without –target is for the machine it runs on; nikaia run shows no Cargo progress
Target::host() reads the triple the compiler was built for, so on Android (aarch64-linux-android), macOS and any triple Rust knows a build without --target is for that machine; before, it fell back to x86_64-linux. nikaia run builds with Cargo’s --quiet: the terminal shows what the program prints and Nikaia’s diagnostics, not “Compiling”/”Finished”.Part I 4.3 says what classes bundle, what goes wrong with them, and what Nikaia writes instead
impl, trait and enum; inheritance, one family tree, virtual calls and hidden references each with what Nikaia does instead (a field, traits, enum and bound calls resolved at build time, one owner and Shared); a list of different types is a list of an enum. No rule changes.standard input and files are untrusted unless marked trusted: true (ADR-365, #649)
assert about them is NK1202 and their maps take the keyed hash; trusted: true at the source says the operator chose the bytes; asset("…") is trusted without a marker. ADR-010 D2, D3 changed by ADR-365. Decided; the build is #649.std: list changes in place, clamp, min/max by key, flat_map and ordered-set walks
Vec::retain, swap(i, j) (i64 positions), reverse, dedup (equal neighbours); clamp on i32, u32, u64, i64; Seq::min_by_key/max_by_key give T? (min the first of equal keys, max the last); Seq::flat_map with a lambda that hands back a list or a sequence; BTreeSet::union, intersection, difference give the elements in order as views, first/last give T?. Fixed: names.first() ?? "none" on a Vec[String] compiles.a tuple may be nullable: (A, B)? is written (ADR-364, #648)
(A, B)? is written as any T? is; p?.0 reaches a part as a nullable and a fallback is a tuple. NK1254 is retired. ADR-278 D1 changed by ADR-364. Decided; the parser, checker and split_once/rsplit_once follow in #648.The prover follows a let mut through each assignment and branch (ADR-361 D9, #642)
x = e, x += e, x -= e or x *= k (k a constant) gives a tracked let mut whole number a new value over the old one. After an if that changes it, it is either side’s value under that side’s condition; an if without else keeps the old value on the other side. A binding handed to a call or changed from a lambda stays out. A claim already decided without these facts does not pay for them.The toolchain’s Nikaia package is lowered at user_parallelism = yes (ADR-363 D1, #646)
nikaia lower-std lowers src/tools at user_parallelism = yes whatever the build’s default (sysroot::tools_build). The sysroot test compares only std’s own modules at both settings, and checks that the committed package.rs is the package lowered at yes.[v; n] is n copies of a value that copies (ADR-362, #643)
[v; n] parses as a list literal of its own and is typed by its use: with no use it is a Vec[T], and where an Array[T, N] is wanted n must be a number the build knows and equal to N (NK1157, naming both). A v that does not copy is refused with NK1280, whose help writes (0..<n).map(fn(_i) { v.clone() }).collect(). It lowers to [v; _] for an array and to vec![v; n] for a list; a negative count aborts.The toolchain’s own Nikaia package is lowered at user_parallelism = yes (ADR-363, #646)
src/tools is lowered at yes whatever the build’s setting, since only the compiler runs it; std’s own modules still lower the same at both settings (ADR-002 D4). The compiler may use par_iter and task::scope. Specified, not built yet.[v; n] is a list literal of n copies of a value that copies; a list of one range is warned (ADR-362, #643, #644)
[v; n] is n copies of v, typed by its use as every list literal is (a Vec, or an Array[T, N] with n equal to N); v must be a value that copies (NK1280, the help writes the .clone()). A list literal whose only element is a range is the warning NK1281. NK1157 covers an n that is not N. Specified, not built yet.nikaia.proofs keeps each loop invariant (ADR-361 D4, #640)
nikaia.proofs under the hash of its loop’s place, with the strategy that found it. A build reads it and checks it with the three queries instead of searching. A missing or wrong entry is searched again, and --locked refuses it. NIKAIA_PROVER_COUNTS counts invariants searched and read.A claim carried back to main’s entry that reads no parameter is not a precondition (ADR-360 D3)
main is a carried-back claim that reads only parameters, at least one of them; one that reads none the prover decides, proved or refuted as any claim is. Built in 679ee67.The prover reads a claim past a for over a range through its invariant (ADR-361 D1, #640)
for i in a..<b and for i in a..b are read as the while they mean: i starts at a, goes up by one each turn and stops once it reaches or passes b. A claim past the loop is proved through the invariant the strategies find and the three queries check. A for over a list or map leaves the claim open, as before.The prover follows a let mut through each assignment (ADR-361 D9, #642)
let mut whole-number binding that no call is handed and no lambda changes through each assignment: after x = x + 1 it is the old value plus 1, after a branch either side’s value, and through a loop by the loop’s invariant. ADR-361 gains D9; ADR-269 D9 notes the change.The prover reads through a while loop: closed forms and intervals find an invariant, three queries check it (ADR-361 D1-D5, D7, #640)
while loop that reads a name the loop changes is asked once more through the loop’s problem in Horn clauses; an invariant from closed forms and intervals is used only once three queries prove it. A let mut whole number is a variable of the proof until it is assigned. --asserts names the invariant and the strategy that found it, or the loop and the names it changes; NIKAIA_PROVER_COUNTS counts loop claims and loop queries. A claim carried back to main’s entry that reads no parameter is not a precondition.A build-time value that runs code is compiled, never interpreted (ADR-321 D1, #636)
comptime initialisers and option defaults that call functions are compiled and run with the program. The build-time evaluator only folds literals, and a call that reaches it is NK1127. A check with no place to compile is NK1127 with ADR-321 D19’s note, and each case the compiler cannot build says why. A ring through called bodies and left-out option defaults is NK1168, said once. A constant read before its turn is worked out first. Builds side by side no longer break each other’s compiled runs.a proposer is a child process, one JSON line per problem each way (ADR-361 D6)
nikaia prove --propose starts each proposer once, writes one JSON line per open loop problem (version, id, problem in SMT-LIB HORN, source, names) to its standard input and reads one line per answer (id, candidates as SMT-LIB terms) from its standard output; a proposer that fails is skipped; --propose=CMD names one for a run.inside its package a callee’s body answers a claim left open; a quotient by a known number is a term; the –asserts report in three groups (#632)
/ and % by a positive constant are read, truncated toward zero (ADR-359 D5). A function whose every exit the prover reads has a result summary, used in a second query only for a claim left open, and written to the ledger as inferred (D1-D3). A divisor pinned by what is known is its value (D6). NK1207 names where a call’s value came from (D8). --asserts lists red, then light blue, then green, and has a row for a precondition of main carried back from a callee. NIKAIA_PROVER_COUNTS=1 counts calls run, second queries and divisor queries.a comptime whose compiled run stops is NK1244 (#637)
comptime whose compiled run stops (an overflow, an index past the end, a panic()) is NK1244, as ADR-321 D16 says; NK1152 keeps the rule’s refusals and the step, memory and depth bounds.NK1201 refuses a range, a tuple, and a struct or enum with no text form in an f-string hole (#638)
impl Error in a hole. The help offers {x:?} for a range or an enum, takes a tuple apart, and names a struct’s fields as holes. Errors, sums of errors, numbers, bool, char, text, a hole asking for :? and a type from another package still pass.the prover reads through a loop: Horn clauses, a portfolio of strategies, every invariant checked, proposers only checked (ADR-361)
a walk of a list of number literals hands its lambdas numbers (#594)
xs.iter() over such a list is copied and filter/find/take_while/skip_while take the item by pattern (ADR-231 D1, D2). let xs = [1, 2, 3]; xs.iter().filter fn(v) { v > 4 } now compiles; a use in the lambda decides the list’s type.A struct, an enum, a range or a tuple in an f-string hole is refused (NK1201)
A method named by its path where a function value is kept runs (#601)
keep(P::add) and One { f: P::add }, where P::add is a method of this program and the field or parameter keeps a function value, are lowered as the closure that calls it, as a named function or a lambda already was: a method that takes self is called on the first parameter, one without by its path. CheckHost::kept_call(func, arity, receiver).the –asserts report puts main’s preconditions between the unproved and the proved, in light blue
the –asserts report lists what is not proved first, in red, and what the build proved last, in green
ADR-359 D5, D7, D11-D13: a quotient of known numbers, a comptime and a call with known arguments are values to the prover (#632 step 1, #633)
/ and % of two known numbers fold in the prover, truncated toward zero; an integer comptime binding is its value.comptime is (sync, empty touch set, ADR-321’s budget and cache), and its value is a fact; a call that cannot run leaves the claim a check and --asserts says why; a run that stops is NK1207 in the program’s words. NK1207’s note names each call value it read.--asserts checks as the build does: files, reads and workshop.A subject T? of main may be left out (ADR-360, #635)
main of type T? may be left out at the end and is then null; usage and help show it as [<name>]. A required subject after a T? one is NK1273; NK1274 is only an option named help.The prover reads an int (#630)
int is read as one about an i64: it becomes a precondition, is carried back and is a fact after it, as ADR-285 D36 says. Before, every claim over an int stayed a check, and a main(a: int) lost its preconditions.int: an i64’s values, the representation the compiler’s; Rust’s other widths are refused (ADR-285 D36-D39, #629, #630)
int is a built-in type with every rule of i64; int and i64 take each other without as, and int is lowered as i64 (D36, D37). int in an extern fn, extern struct or extern { } declaration is NK1262 (D38).as out of a number with no annotation is checked as one out of a typed number: v as u8 stops where the value does not fit instead of truncating (#629 D-1). A method called on such a number no longer makes the function async (#629 D-2).usize, isize, i8, i16, u16, i128 and u128 are NK1135 outside an extern declaration (#629 D-3).ADR-360: a subject T? of main may be left out; NK1274 is an option named help (#635)
main of type T? may be left out at the end and is then null; it stands after every required subject and before a last Vec[T], and a required subject after it is NK1273. The clause on two options that are one name once _ is - is removed (a name cannot hold -); NK1274 is an option named help. Specified; the T? subject is not built yet.main’s parameters are the command line (ADR-360, #635)
main may take parameters: subjects are positional words, options --name value or --name=value, a bool option a bare --name; -- ends named words and -3 is a value. A parameter no word can be is NK1273, an option named help is NK1274.main’s preconditions are carried back as any function’s are and checked where the words arrive; a wrong word or a broken precondition prints one line and the usage and ends the program with exit status 2 before the body runs. --help/-h print the help from the /// lines, types, defaults and preconditions. --asserts reports each as a precondition of main, checked where the words arrive.main’s parameters are the command line (ADR-360, #635)
main’s subject parameters are the positional words, its options --name value with their defaults; a parameter is int, a fixed width, f64, bool, String, one behind ?, or a last Vec[T] (NK1273, NK1274). main’s preconditions are derived as every function’s are and checked where the words arrive; a wrong word or a breach ends the program with exit status 2 before the body runs (Part III 14.2, A.2). --help is generated from ///, types, defaults and preconditions. Specified; not built yet.A named function handed to a parameter the callee keeps runs (#601)
keep(double)) is lowered as the closure that calls it, as a lambda passed there is; before, the checker accepted it and rustc refused it. A method used as a value (keep(P::add), One { f: P::add }) still fails the same way (#601 stays open).A call with known arguments that a claim needs is compiled and run while building (ADR-359 D11-D13, #633)
comptime is (ADR-321), never interpreted: sync, an empty touch set, under ADR-321’s budget, cached; its value is a fact.NK1207. Running such calls as an optimisation where no claim needs them is #634.ref on a word refused at every position (#628)
NK1257 covers parameters, fields, results, trait method parameters and results, and enums whose variants hold nothing (ADR-354 D2); extern "C" keeps ref i32 / ref mut i32.tools/*.nika and tests/language migrated; lowering defect D42 filed (an untyped let x = m[k] ?? return over a map of scalars keeps a &T).pub extern fn in ADR-282 D28, Part I 2.2 and Part III’s NK1261 entry (ADR-324 D6).NK1261 (the warning for ref String inside Nikaia) is not built yet: String does not yet do everything ref String does; #628 keeps the build order.Inside its package a callee’s body answers a claim that needs it; a quotient by a known number is a term; across a package, a hint (ADR-359, #632)
inferred (an observation); / and % by a positive constant or a pinned divisor; a comptime binding is its value; NK1263 hint across a package read from a path; a new refutation is fixed, refuted-claims = "warn" is for an emergency.NK1263, NK1207’s note). Specification only; the build is #632’s To build list.The prover reads a call inside a let’s value by its callee’s ensures (ADR-314 D3, #631)
let’s value is a whole number of the proof, known by its callee’s postconditions (Part III, assert paragraph).--asserts names the binding a claim reads that the prover does not.int: the values of an i64, its representation the compiler’s choice (#630)
int beside the five fixed widths; it holds an i64’s values and aborts where an i64 would, int and i64 take each other without as, its representation is the compiler’s; a fixed width is exact (ADR-285 D36, D37, D39).NK1262, an int in an extern "C" signature or a described Rust contract (ADR-285 D38).ref String only at the C boundary; ref on a word refused everywhere (#628)
ref String is redundant (warning NK1261), the ledger states what a published signature does; at extern "C" and pub extern "C" fn a view is written, required and checked.ref on a word by its kind is NK1257 at a let, parameter, field and result outside extern "C".NK1257 was listed as NK1258; NK1261 added.a text literal handed to a String option is a String (ADR-282 D4)
greet("Nika"; greeting: "Hallo") against greeting: String was NK1106; a literal at an option is now constructed there, as at a parameter. The guide’s and README’s examples take String, not ref String.every target Rust knows is a target; Android has short names (ADR-037 D9)
target takes aarch64-android, x86_64-android, armv7-android and every triple rustc --print target-list prints; a bare-metal (-none, -uefi) triple is refused with the reason. Build profiles (main loop or interrupts) are left to a record of their own.check: a loop body’s tail if or match, and a discarded choice’s arms, ask no common type (#596)
for or while body is never used, so an if or match
at its end is a statement: arms of different types (m.insert(..) and
n.insert(..)) are no longer NK1245. A discarded if or match hands
this on to its arms, so an else if chain and nested tails are statements
too.a parameter of code called in a throws function hands its failure on; from(p) chains through a parameter handed on and a mut parameter’s place (ADR-351 D3, D4, part of #620)
throws function hands its failure on (f()?); before, the failure was dropped.catch over now(fail) is told (NK2402) what fail gained since the committed ledger, although now still says from(f).now(g) inside twice(g)) is twice’s from(g), not "?"; a catch over twice(fail) needs no else.let mut name of code handed to a mut parameter of this package’s own function holds what that parameter’s place holds, not "?".a Rust adapter names a tools map’s hasher, and the tools lower the same in any file order (ADR-010 D9, #581 steps 8-10)
sysroot::lower_tools_files lowers the tools package from a list of files in
the order given; lower_tools_at sorts and calls it. Read backwards, the
compiler’s own tools lower to the same sections (map_trust.rs).pub struct with maps as HmNames (keyed) or
HmNames<nikaia_std::hash::Fast, nikaia_std::hash::Fast>; both compile.BTreeSet switched to HashSet lowers keyed and
costs +0.10 % lowering ty.nika, so the tools keep BTreeSet.io::IoError needs
use std::io.A hand-over under a view is NK2305, a move into a local keeps the buffer, a pub type keeps a place per view field (ADR-353 D3, D6, D7, #614)
take(y), v.push(y)) while a view that may point into it is still read is NK2305 at the call (D6).let z = y while a view of y is read puts y’s buffer into the frame’s keep from its let, with no copy; z is followed, so changing it in place is NK2305 (D3).pub struct keeps one lifetime per view field, and a reading package takes them from the ledger’s tethered; the ledger’s version is 4, nikaia.derived stays at 3 (D7).check: what the core crossed back into Rust for, in Nikaia (C-7 U-d1, #125, #558)
A parameter may take a tuple apart, in a function and a lambda (#623)
fn dist((x, y): (f64, f64)), fn((name, v)); it stays one parameter; ref/mut belong to the whole; the ledger names the parts; a variant is NK1259; NK1101’s help writes fn((a, b)).A catch naming every error type is complete; kept code’s set belongs to its storage (#625, #620)
match over a catch’s error that names every non-fatal type arriving needs no else; "?" or an open from(p) needs one; a grown set refuses such a match (NK1151, narrated), and NK2404 warns the author of a pub function whose set grew. ADR-351 D8: kept code lives in a value something holds; its set belongs to the storage (handles share it), the ledger’s new code key says what a function puts where, and it lowers without a box.Lowering defects D2, D4, D9, D19; four tools workarounds removed
for over a ref Vec[T] whose T copies by declaration (the package’s own Copy type, or a described one whose ledger says copies, such as Symbol) binds the value, as one over numbers does (D4).strip_prefix and strip_suffix in a loop among them (was NK2105; D41); throws.nika drops its prefix.clone().selfhosting_defects.rs.tools/*.nika: the D36 loops (check_core_places_bound, check_core_flow_part), the D38 helper check_core_call_after, the D34 inline comparison and the D19 one-arm-per-string match give way to the plain forms.a viewed argument given a new value in a nested block lives in the frame’s keep; a union field takes one lifetime over its group (ADR-353 D2, D3, part of #614)
let (if c { y = v }), while a view of it made outside is used afterwards, lives in the frame’s keep from its let; the assignment puts the new value there too. No copy; --tethers shows it.returns = "best: ref(a | b), other: ref(c)") gives that group one lifetime and every other parameter its own: fn pick<'r, 'r1>(a: &'r str, b: &'r str, c: &'r1 str) -> Pick<'r, 'r1>.a function’s set names the code it was given, from(p) (ADR-351 D4, part of #620)
pub function (or one used as a value) that calls code it was given
publishes throws = ["from(p)"], read back as itself; a caller replaces it
with the set of what it passed, and a catch over the call takes that set
by name. An unreplaced from(p) is open, as ? is.?.for over a field path (for run in jobs.all) takes the field’s set, and
a list or map of code in a struct literal is a place too.A pub type keeps a place per view field (#614)
tethered lists them in order (ledger version 4). An enum and a thrown type keep one.Lowering defects D10, D11b, D18, D26, D37, D39, D40
catch is written in brackets (D39).ref field of a struct literal is lent (D37, checker fact lent_fields).?? on a type of the program’s own refuses a fallback of another type, a number among them (NK1103, Part I 3.5; D10).struct or enum of the package with no impl Error in any file is refused (NK1161; D11b).mut parameter that is a view, handed back, is refused (NK1131; D26).let mut is owned, so the local can be changed (D40).NK1101, help fn(pair); D18, #623).a map’s read past a jump, bound by a let to a declared owned type, is refused or copied out (ADR-293 D15)
let x: T = m.get(k) ?? return .. was accepted and lowered to a &T where T was declared; it now gets the assignment’s rule: a value that copies is copied out, another is NK1102 with ?.clone() to write.check: what a test proves not null in Nikaia (C-7 U-d0, #125, #558)
check/narrow.rs (ADR-350) moves to tools/check_narrow.nika; its four Checker fields travel with the baton as CheckState.narrowing, and six host methods are gone.Handing viewed data to a new owner is NK2305 (#614)
NK2305 at the call; a move into another variable of the same function, or to a parameter lowered as a view, gives the buffer to the keep. Part I 6.6, Part III C.3.std ledger: Restart::decide keeps its history
a function value’s set meets what reaches it: mut names, push, field assignment and with carry the union (ADR-351 D3, part of #620)
a view’s argument may be given a new value while the view is used; a change in place under a view is NK2305 (ADR-353 D3, D4, part of #614)
let r = longest(x, y); y = "replaced".clone(); println(r) compiles: an
assignment in the block that declares y lowers as a binding of its own,
so the old text lives on with no copy, and a let mut whose every change
became such a binding loses its mut (view_changes.rs).NK2305: a change in place - a changing method, an assignment to a part -
of data a view made by a call or read off a place (ADR-354) may point into,
while the view is used later or by the loop around the change, is refused
at the change, naming the view, the call, the later use and the callee’s
way out.rustc.check: the walk’s hub and dispatcher in Nikaia (U-c11, #125, #558)
check_core_expr and check_core_value_of move to tools/check_core_expr.nika with the arms for the literals, a name, a path, -/!/&, ?, throw, return, break/continue, dsl and asm; the host method expr is deleted and the arms call each other directly.a parameter handed to a trait’s method by value is kept
host.go(f) under [H: A]) keeps the caller’s by-value parameter, and the lowering writes it as a value, not a & the method does not take (found in U-c11: deleting CheckHost::expr made emit_match_subject lower flow: &EmitFlow).a struct of views takes one lifetime per view field, and a result’s fields their parameters’ (ADR-353 D1, part of #614)
Pair<'a, 'a1>); every mention, impl and tether impl writes as many.returns names one
parameter per field, gives each parameter its own lifetime
(fn pair<'r, 'r1>(a: &'r str, b: &'r1 str) -> Pair<'r, 'r1>): a caller
holding pair(x, y).left may give y a new value (#614, To build 3).lowering defects: with keeps its base, a typed throw into a boxed trait channel, names_read sees statements, a match in a guard names the brackets
p with { .. } keeps p: a by-value parameter it reads whole stays a value (lowering-defects D1).throw in a function whose channel is the box is raised into it (D11).names_read walks block statements; a guard-only copy is not written (#622).match in a guard or condition is refused with a message naming the parentheses (D16).Owned text is lent to strip_prefix and strip_suffix; a literal asked of a list of text is a view
x.strip_prefix(p) and x.strip_suffix(p) with an owned String p lower with &p, as starts_with does (check_values.nika); names.contains("?") on a Vec[String] takes the literal as a view of text (check_core_method.nika). Both reached rustc from tools/*.nika (#620). Test: selfhosting_defects.a result’s views keep their own sources by path; the ledger writes them per path (ADR-353 D1, D5, part of #614)
left:a beside a plain
a, so a view taken out of pair(x, y).left points into x alone. A path
is one field long (a deeper view folds into its field’s), which keeps the
entries finite and the fixpoint short.returns = "left: ref(a), right: ref(b)",
read back at callers in other files. The lowering still ties a struct’s
views to one lifetime (step 3 of #614’s list).s.strip_prefix(f"id{n}")
lowers to strip_prefix(&format!(..)), which rustc refused before (E0277).check: operators, ??, the lambda and spawn in Nikaia (U-c10, #125, #558)
Binary, Coalesce, Closure and Spawn of the checker’s walk move to tools/check_core_ops.nika; narrowing stays in check/narrow.rs, asked through host.proved_here and the new host methods a_proof_taken_back and a_type_that_does_not_compare.a function named as a value, a published field and an open set make a catch need else (ADR-351 D4, part of #620)
list patterns keep elements, guards read copies; a struct or list proved not null is a T (#622, #621)
a result built from a call handing back views of its arguments makes no buffer; a method’s result may view its receiver and a parameter (ADR-353 D1, part of #614)
longest(longest(x, y), z) points into x, y and z and takes no keep:
an argument that is itself a call handing back a view is no buffer made in
the call (tools/buffers.nika). It lowered as tethered before and failed
below Nikaia (E0621).&'r self beside the parameter.impl Hash: a hand-written hash for a type with a hand-written equality (#591)
Hash is a trait of the language, hash(ref self, mut into: Hasher) sync with into.add(x); a declared type with an impl Eq is a hash map’s key only through an impl Hash. NK1249 and NK1260 cover it.check: match, if, select, overlap, catch and unsafe in Nikaia (U-c8, #125, #558)
Match, If, Select, Overlap, TryCatch and Unsafe of the checker’s walk move to tools/check_core_flow.nika; what a test proves not null (ADR-350) stays in check/narrow.rs, asked through the host methods proved_here and proved_from_here. No program’s findings or lowering change.spec: a hash map hashes a declared type from its fields (ADR-010 D11, NK1260, #591)
HashMap/HashSet key is checked by the structural walk == uses; a declared struct or enum hashes, derived from its fields in the order written, where every field hashes; a float, a map, a set and a declared type with a hand-written impl Eq (ADR-341 D5) do not. A key that does not hash is NK1260, at every position a type is written. Library types answer from a new ledger column hashes. Part I (map keys), Part III 13.5 and C.3; ADR-204 §4 and ADR-341 §6 point to it. Not built yet.a parameter of code fails with what its callers hand it (ADR-351 D4, part of #620)
pub and not used as a value is a place of the error-set calculation: every call in the package adds what it hands that parameter, and a call through it fails with that union, so a catch takes the errors by name.a call through a variable or a list element fails with what it was given (ADR-351 D1, D4, part of #620)
for takes from the list, fails with the union of the functions given there, and a catch takes them by name or is NK1134 when none fails. Lists, if and match arms meet; a mut name stays unnamed (?) until reassignment is counted.a let and a for head take a nested tuple of names; a variant there is NK1259 (ADR-356, #611)
a call through a function field fails with what was stored there (ADR-351 D1-D3, part of #620)
List patterns, and .. as the rest of positional values (ADR-355, #618)
[], [x], [first, .., last], [h, ..rest] match a Vec[T], an Array[T] or a view; ..rest is a view ref Array[T]; one .. per pattern (NK1258); a list match needs else.(a, ..) and E::V(a, ..) match the remaining values; NK1255 is only .. on a variant without values.A result may point into several parameters (ADR-353 D1, D2, #614)
NK2302 no longer refuses longest or pair: the ledger lists every parameter the result reaches, and the lowering ties them and the result to one buffer group.
An element or a field read off a place is a view under ref T (ADR-354, #613)
let y: ref Expr = given[0] over a given: ref Vec[Expr], and given[0] as
an arm under ref Expr?, are views of the element, lowered with &; both were
NK1103. A word by its kind written so, let y: ref i64 = given[0], is
NK1257, whose help writes let y = given[0]. A generic ref T is a view
whatever T becomes (Part I 6.5).
spec: a result’s views point where the body says; a viewed buffer outlives its owner’s name (ADR-353, #614)
NK2305, at the change. The ledger writes each view’s sources (-> ref(a | b) String, returns = "left: ref(a), right: ref(b)"). Part I 6.6, Part III 13.5 and C.3. Not built yet.null is a pattern in a match over a T? (ADR-352)
match count { null => "unknown", 0 => "none", n => f"{n}" }: with a
null arm the other arms match the T - a bare name binds it, a literal
compares with it - and a guard works on any arm (null if c =>). An
unguarded null arm (or else) and a catch-all are a complete match
(NK1151 names null where it is missing). A null arm over a value that
is never null is the new NK1256. Lowered to None / Some(…).rs2nika: None => and None if c => stay null arms (R71), and
Some(p) => becomes p => (R72).check: a test that proves a T? not null makes it a T (ADR-350, #619)
| The checker reads a local or parameter as T where a test proves it not null (&&, | , branch, else, while body, after an early exit); a write after the test takes it back and NK1102 names the assignment. The lowering reads it as x.unwrap() (text: x.as_ref().unwrap()). Structs and lists keep T? below for now. rs2nika writes if let Some(x) = x as if x != null. |
D36: a ?. reach on get, first or last takes no extra .as_ref()
m.get(k)?.clone() ?? x cloned a &&T into a &T; a read that already
lends its value is now matched as it is, so the clone is the value.
check: fields, struct literals, with, an index, a cast and a range, in Nikaia (U-c9, #125, #558)
Field | SafeField, StructLit, With, Index, Cast and Range move to tools/check_core_places.nika (U-c9); the host gains free_calls. No program’s findings or lowering change.spec: a function value keeps the error set inferred for it (ADR-351, #620)
from(p). supervisor::run throws a child’s fatal error itself and supervisor::Handed is withdrawn (ADR-346 D7 changed). A net::serve handler’s error is a detached task’s (ADR-346 D6). Part I 5.4 C and 7.1, Part II 12.8, Part III 13.5 and net. Not built yet.spec: a test that proves a T? not null makes it a T (ADR-350, #619)
| ADR-350 records the owner’s decision on #619: what a test proves, the compiler uses; Part I 2.3 states where the proof holds (&&, | , the branch, a while body, the rest of a block after an early exit) and that a write takes it back. |
check: a statement and a block, in Nikaia (U-c7, #125, #558)
stmt (one function per Stmt variant) and block move to tools/check_core_stmt.nika; the host method block is gone, and CheckState holds got_past_a_jump and discarded. No change in what a program means.std: text search is inlined; brc’s naive.nika reads the temperature as naive.rs does
nikaia_std::search’s find, rfind, replacen and split_at are #[inline]: a call from a program crossed the crate boundary for every line (about 27 instructions a row in brc’s naive program).naive.nika reads the temperature digit by digit, the algorithm naive.rs uses, instead of replace and parse_i32: 597.8 instructions a row against 860.3, at 320 tokens against 486.Text search: a one-character pattern is a char, and split_at takes find’s i64 (#616, #617)
split, rsplit, splitn, replace, contains, starts_with, ends_with, matches, trim_start_matches, trim_end_matches, strip_prefix, strip_suffix) reaches Rust as that char (split(';')): Rust builds a two-way searcher for every &str pattern and searches a char with memchr. std’s find, rfind and replacen take the same path for a one-byte needle (#617).text.split_at(at) lowers to nikaia_std::search::split_at, which takes the i64 find gives; it reached rustc as a usize mismatch (#616).benches/brc/naive.nika, the naive program in Nikaia: 1 022.9 instructions a row (1 726.9 before these two), naive.rs 860.6, at 291 tokens against 486. scripts/tokens.py is scripts/tokens.nika.check: a method call, x.m(…) and x?.m(…), in Nikaia (U-c6, #125, #558)
MethodCall | SafeMethod of the checker’s walk moves to tools/check_core_method.nika (about 670 Rust lines out, with the dead wrappers a_safe_call, grammar_call, call_on, a_called_field, method and the enum Reached). Nothing a program sees changes: lower-std and the oracle over 143 files are byte-identical.cache: evict on write, nikaia cache clean (#615)
nikaia cache clean empties the user cache.check: a method call on a known receiver, and ?., in Nikaia (U-c5, #125, #558)
call_on and a_safe_call move to tools/check_core_call_on.nika
(about 690 Rust lines out, with 21 wrappers and helpers left dead). The
argument list stays the one without the receiver, as in Rust.a_lambda_that_pauses_at_an_entry (stays Rust) and
a_task_of_a_scope, the task::Scope::spawn half that belongs to the task
work (#605).lower-std is byte-identical between the
old and the new binary, and the C-6 oracle finds 0 of 143 files differing.task: a scope’s function may throw; a sum keeps a later failure of another member (#605)
task::scope’s function may throw (fn(ref task::Scope) -> T throws): its error is the scope’s, in the place after every task it started (ADR-346 D7, Part II 12.7), at both settings and with tasks that pause. In a sum channel, a later failure of a member other than the first one’s joins the first in the envelope’s list of other-typed failures (Thrown::joined_by_other) instead of being dropped, and prints under it. Open: a catch on supervisor::run and the type of its error (decision basis on #605).
A variant’s rest pattern E::V(..) (ADR-349, NK1255, #610)
The owner’s decision on #610: E::V(..) matches a variant whatever values it carries and binds none, as .. is already the rest of a struct’s fields; it lowers to Rust’s E::V(..) and NK1253’s count is not asked of it. NK1255 refuses it on a variant without values, and refuses a positional rest E::V(a, ..) (left undecided) with a message saying it is not part of the language. Part I 3.4’s pattern table and Part III’s code table; lowering-defects D27 built (match_patterns.rs).
Program end: skip-free-at-exit:on leaves main’s memory to the OS (ADR-348, #607)
The owner’s decision on #607: freeing stays the default; the optimization word skip-free-at-exit:on (nikaia.toml optimization, or --optimization) writes an allocator into the program whose free is skipped from the end of main’s last statement until the drain. Every value still dies and every cleanup runs; output, exit status and the detached-task log are unchanged (process_exit.rs). brc: 300 192 748 instructions by default, 300 184 756 with the option.
check: a method’s function parameter is counted past its receiver, so a lambda it only runs is not kept (#125)
check_core_args_runs (0.0.912, U-c3) counted self as a call’s first argument, so a lambda handed to a method’s run parameter was recorded as kept and lowered as Kept<…> where the method takes impl Fn (red language: defects_found_running.nika, supervision.nika). It now counts past the receiver, as Rust’s arguments() did.
task: a scope’s catch lets its tasks borrow; a sum joins; a fatal error keeps its site
check: a free call, a tuple and a list literal in Nikaia (U-c4, #125, #558)
call and the arms Call, ListLit and Tuple move to tools/check_core_call.nika; arguments, an_attribute_read, a_call_that_hands_its_number_back, a_standard_bound_not_met and the task::scope half of a call (#605) are host methods.ref Vec[T] parameter is a slice in the declaration, as in the impl; it was &Vec<T>, and a call from a function holding a &[T] did not compile.task: task::scope hands its tasks’ errors on
The pool’s error registration is quiet in rustc
lowering: a field option of a get local past a jump (D35); (A, B)? refused with NK1254 (D5)
let c = m.get(k) ?? return then c.f ?? return opens the field (.as_ref()/.as_deref()) instead of moving out of the reference (E0507).(A, B)? parses and is refused with NK1254, naming a struct and Name? or (A?, B?) (Part I 2.3, Part III code table).check: a call’s arguments, a door over several locks, a list literal and a called field in Nikaia (U-c3, #125, #558)
A statement match arm is a statement
match whose body hands back a value (k => seen.insert(k, v)) drops it, so it agrees with else => {} (lowering-defects D34).check: a comptime and a dsl block over a grammar in Nikaia (U-c2, #125, #558)
task: a typed error crosses the pool; a supervisor’s hand-over carries the child’s error (#605)
A variant pattern names every part
NK1253: a pattern of a positional variant with a different number of parts than the variant carries (E::A(_) against A(i64, i64)) is refused by the checker instead of reaching rustc (lowering-defects D33).check: the leaves of the core in Nikaia (U-c1, #125, #558)
a_field_of_a_borrowed_subject, a_field_of_a_lent_place_handed_back, a_kept_lambda, literal_by_use, holes, lambda, returns and an_assert move to tools/check_core_leaves.nika; the host answers holes_read. Findings and lowered Rust are unchanged (oracle: 143 files, 0 differ).task: a program’s own fatal types are fatal in tasks; a supervisor hands a fatal error on as supervisor::Handed (#605)
check: the arguments of a call in Nikaia (#125, #558)
arguments moves into tools/check_arguments.nika; its dead wrappers leave the checker.lowering: an option in a list element past a jump, a match on what get handed back, an iterator’s text beside a literal (D30, D31, D32)
xs[i].f ?? continue clones the option out of the element. D30: let held = m.get(k) ?? continue then match held writes no ref. D32: xs.iter().find_map(..) ?? "lit" maps the answer to text of its own, no longer ambiguous in rustc.check: a view kept where the receiver says text in Nikaia (#125, #558)
a_view_kept_where_the_receiver_says_text moves from the checker into tools/check_arm_leaves.nika; its message is a lambda that captures the callee and the parameter.task: detached errors reach the log at main’s end; a supervisor hands a fatal Exit on (#605)
check: the leaves of a choice, arms asked one by one, map keys and handing over in Nikaia (#125, #558)
value_leaves, arms_to_ask, note_mixed, views_meet_owned_text, expect_kept, span_at, a_map_key, hands_over and the settle of a_settle_in_this_function move from the checker into tools/check_arm_leaves.nika; arm_types and arm_disagreements are CheckState’s, and ParsedView carries the parser’s expression spans.check: overflow, slot wraps, Copy and the collected types in Nikaia (#558, #125)
check: NK2109 on every path (#605)
contracts: filter_map/find_map lambdas read as lambdas; the project sync inference reads described crates (#579)
Seq::filter_map and Seq::find_map declare f: fn(T) -> (U?): written fn(T) -> U? it parsed as an optional function and from(f) refused it (red develop).sync inference reads contracts/<crate>.contracts as the check does: a function calling only a described sync entry is no longer inferred to pause (NK2202 on its sync callers, D29).check: NK2109, a forgotten task handle (#605)
let h = spawn … whose name no later statement of its block mentions is refused (NK2109, ADR-346 D3); a statement spawn stays detached.task: join(within: d) (#605)
h.join(within: d) waits at most d; on time-out the task is cancelled and the call throws task::TimedOut (ADR-346 D4, C code 1101).match error arm names a struct error whole (task::TimedOut, task::Crashed { .. }).lowering: a let-bound function value is lent its views, owned text lent to a pattern; Seq::find_map, Args::len (#579)
& a named function’s call writes for a ref parameter: f(xs[0], 1) and f(s, 1) reached rustc as mismatched types (D20).starts_with, ends_with or contains is lent, as to any ref String parameter, instead of NK1102 with write ref (D28).Seq::find_map, and len/is_empty on cli::Args.check: whether a type compares, in Nikaia (#125, #558)
task: a catch at join names the body’s error; a failing task on the pool (#605)
catch at join whose match error names the body’s error types opens the starter’s channel by them (a sum, an envelope, or the box via std::error::holds/opened), ADR-346 D5.user_parallelism = yes a body failing in the opaque channel starts a task::BoxedTask: the error crosses as its message and an Exit’s code.task: a task’s error reaches join, and detach (#605)
check: a_fatal_error_passes, an_exit_code_out_of_range and an_operator_needs_a_bound in Nikaia (#125, #558)
An enum with one variant is written by its type’s name (ADR-347, #608)
Exit(c) for Exit::Code(c), Pos { x, y } for Pos::At { x, y }, Done for Done::Only, at the value and in a pattern; process::exit(code) throws process::Exit(code).NK1252: the short form of an enum with several variants; the message names the long forms.spec: every task error has a receiver, ADR-346 (#605)
check: an arm of an unworked-out type beside a null is wrapped, so a lambda’s if/else with null lowers (#586)
if/match arm whose type the checker did not work out, beside a null arm, is written as Some(..): a filter_map lambda if … { x / 2 } else { null } no longer reaches rustc with mixed arms.check: a T? as an operand or a hole, a list as a text pattern and impl Fatal without impl Error are refused (#586)
NK1102: a T? operand of arithmetic, an ordering or a bit operator (also after != null, and a name a match arm binds over a T?), and a T? hole of an f"…", which reached rustc.NK1102: a list handed to starts_with/ends_with/contains on text (D17).NK1164: impl Fatal for T without impl Error for T (ADR-345 D1).clippy::never_loop is allowed on the lowered tools (a for that ends with break).std: reductions and searches on Seq, rsplit and split_at on text
Seq gains any, all, find, position (an i64?), filter_map, last, take_while, skip_while, max, min and sum; text gains rsplit and split_at (two views). The lambda of find, take_while and skip_while takes the element as filter’s does.json: an explicit null takes a field’s default (ADR-344 D4, #604)
json::read: a member that is null for a field that is not a T? takes the field’s default if it has one, and is an error only without one (ADR-344 D4, D8; Part III 17.3).json: a member the type has no field for is skipped (ADR-344 D8, #604)
json::read[T] skips an object member T has no field for instead of refusing it (ADR-344 D8, Part III 17.3); a deliberate departure from ADR-299 D17, since JSON from outside grows by protocol extensions.Fatal errors; process::exit throws process::Exit (ADR-345, #606)
json: a missing member takes the default or null in a T?, json::Value put back (#604)
process::exit built (ADR-343)
process::exit(code) built: main ends with the code after its cleanups and the drain; the ledger’s exits column; NK2607 in a task; a constant code outside 0..=255 is NK1116 (#603).check: payload_of, claims_report, byte_literal, peeled and an_or_a in Nikaia (#125, #558)
json: an official package in packages/json, read[T]/write[T] by ADR-299’s rules, a build-time schema, json::Value (ADR-344, #604)
json::read[T], json::write[T], json::parse into json::Value, a JSON Schema checked at build time; NK1250, NK1251.process::exit(code) decided (ADR-343)
process::exit(code) ends main with that code on the path a normal end of main takes; the ledger’s exits column; NK2607 refuses code that may end the program in a task (Part III 13.5, 17.1, A.2, C.3; Part I 8.2).check: None is answered with null (#125)
NK1117 on None, nil or Nil says that the absent value is written null.std: rfind and replacen on text, is_uppercase, is_lowercase, is_ascii_digit on scalar (#125)
s.rfind(needle) -> i64? and s.replacen(from, to, count: i64) -> String, counting in i64 as find does.scalar answers is_uppercase, is_lowercase and is_ascii_digit.std: cli::args().nth(at) takes an i64 (#125)
cli::args() returns cli::Args, whose nth counts in i64 as the ledger says; nth(at) with at: i64 lowered to Rust’s usize one (E0308).emit, parser: a composite ?? fallback is bracketed, and a - after one is refused (#125)
x ?? s.len() and x ?? (0 - 1) lower to (s.len() as i64).into() and (0 - 1).into() instead of invalid or wrong Rust.a ?? 0 - 1 is refused as ADR-279 D1 says, instead of being read as a ?? 0 followed by a statement -1.Check: articles, tail_of and unpromised in Nikaia
articles moves to tools/check_words.nika, tail_of to check_types::tail_value_of (returning ref Expr?), and unpromised calls the new Sync::is_unpromised (#125, #558).emit: a cast is cast once and borrowed whole (lowering defects D7, D8; #125)
xs.len() as i64 lowers to xs.len() as i64, not (xs.len() as i64) as i64 (clippy unnecessary_cast).m.get(x.start as i64), lowers to &(x.start as i64), not &x.start as i64 (E0606).Check leaves in Nikaia: wholly_known, a_map, a_copied_part, is_a_literal_other_than
wholly_known, a_map, a_copied_part and is_a_literal_other_than move from check/mod.rs to tools/check_types.nika; several_errors and is_a_whole_number call the Nikaia functions that already existed (#125, #558).Checker: an open-number arm is asked against the written type
if or match arm that is a number nothing typed, beside an arm of a type, is asked against the written type of an annotated let, a return or an argument as a number written there would be: let y: String = if c { "a".clone() } else { 1 } is NK1103 as let s: String = 3 is (#600).Expressions carry spans
Parsed::expr_spans, #107); the type checker’s refusals about one expression (an arm of an if or match asked where its value goes: NK1102, NK1103, NK1104, NK1106, NK1245; an argument, an assigned value, a field value, a condition) put the caret on that expression instead of the whole statement. A node the compiler builds has no entry and is reported at its statement.checker: node-level facts keyed by NodeId (#558)
checker: casts keyed by the operand’s node (#558)
checker: a view in a local struct that never leaves is no store (#585)
lowering: a view into a String parameter only read is lent as it is (#598)
checker: NK1245’s help for disagreeing arms asks for no annotation
checker: into_either over an if/match of views and text is each arm’s; an annotated mixed let works (Part of #562)
value.into_either() / into_either_maybe() over an if or a match whose arms are ref String, String, String? or null is typed String / String? and the arms are wrapped one by one, so let y: String = if c { s } else { owned } works and no message names the generated call.checker: a mixed let is bound as the view it may be; the position it flows into decides (Part of #562)
let y = if c { s } else { owned } binds y as ref String (ref String? beside null) whatever the function returns; return y in a pub fn … -> String is NK1104 naming .clone(), and a parameter both kinds flow into takes it as it is.lowering: a mixed text position wraps each arm of an if/match; ref String and String arms meet at text (#562)
EitherText, Option<EitherText>) is wrapped arm by arm: each leaf of an if/match (recursively through nested choices and blocks) is either(…), either_maybe(…) in a String? position, null stays None; the checker records the leaves (mixed_leaves, mixed_choices) where a position takes a view arm beside text of its own (return, tail, field).let y = if c { s } else { owned } is text: a ref String arm and a String arm meet at String (or String? beside null) in a function whose result is mixed or is no text; a pub fn … -> String keeps the refusal (ADR-282 D22, D25).checker: an if/match value is asked where it goes; arms that disagree are NK1245 (#596)
if or match only when its arms agreed or met at text or null; arms that were known and disagreed made the value unknown, and the mistake (return if c { s } else { 1 }, a view in an arm of a match that returns String) reached rustc. Where the value goes into a position with a type, each arm is now asked as a value written there (NK1104 return and tail, NK1103 annotated let, NK1102 argument, NK1106 field), arms that leave are skipped, and arms that are all known and disagree with no position to ask are NK1245 at the if/match, naming the arms’ types. Part III C.4 says so.ADR-327 D11: update_all copies only where both values copy; a read-only door never poisons (#490)
ADR-327 D11: an update of a value that copies works on a copy, so a panic in it poisons nothing (#490)
ADR-327 D8-D9: a copy toward Rust is written, never inserted (NK2506); a value that left its task counts against its receiver (#490)
ADR-342 and ADR-327 D8-D10: a chain runs element by element; a task’s block holds nothing that outlives it (#595, #490)
NK2302: a std lookup typed by the receiver’s type parameter points into the receiver alone (#585 item 4)
nikaia check: the checker’s findings for one file, nothing written (#590)
Eq and Ord: bounds, operators, compare, impl Eq / impl Ord, NK1247 for declared keys, NK1248, NK1249, NK1164 (ADR-341, #592)
ADR-341: Eq and Ord are sync and lower to the standard library’s traits (#592)
ADR-341: Eq and Ord are traits of the language; a bound gives the operators; impl Eq / impl Ord for declared types (#592)
check: an ordered map’s key must have an order, NK1247, ledger column orders (#589, ADR-010 D10)
tools: trust and emit_locals key their maps by Symbol, no copy of the name (#589)
Shapes of the held patch: no rustc error for a barrier handed a self map, a pub function’s map handed to a private one, or a grammar rule’s type (ADR-010 D9, #581 step 6 fixes)
self.a == self.b) does the same for its type: the impl’s parameters are the type’s and no signature can say two of them are one.The ledger’s maps key: one line per group of map positions, from(..) or keyed; a function declares one hidden parameter per group its body joins (ADR-010 D9, #581 step 6)
A pub type declares one hidden parameter per map position, in field order (ADR-010 D9, #581 step 5 fix)
pub struct are two hidden parameters, as a function’s signature counts them; a function still shares one parameter between positions of one written type.Barriers: a map in a trait method’s signature, a described crate’s, or handed to one is the keyed map (ADR-010 D9, #581 step 7)
pub function that hands a map to such a method, and a method whose result alone holds a map, have the keyed map at those positions instead of a hidden parameter; a program that calls one, or a described crate’s function taking a map, is no more trusted than the keyed hash; a trait implemented in a package that reads a socket keys the program that uses it.A map at a package’s boundary is a type parameter each use fills (ADR-010 D9, #581 step 5)
Map classes: a map keeps the fast hash across a private function’s parameter and result (ADR-010 D9, #581 step 4)
NK2302 payoff: two world clones return the view (ADR-283 D25, #580 step 8)
NK2302: a result of a struct another file declares is walked like one declared here (ADR-283 D25, #580 B12)
Package-wide trust: a map is keyed in every file when any file of the package reads an untrusted source (ADR-010 D9, #581 step 1)
NK2302: the ledger, the lowering and the message follow the one source (ADR-283 D25-D26, #580 steps 5-7)
'static for none, the keep’s for a body-made buffer), and the message puts a caret under every way out.One map, one hasher: decided per package, a type parameter at the package boundary, decided (ADR-010 D9, #581)
NK2302: a result points into the one parameter its body hands back, checked (ADR-283 D25-D26, #580 steps 1-4)
NK2302: a result points into the one parameter its body hands back, decided (ADR-283 D25-D26, #580)
emit: the flow of the core is Nikaia: Closure, Binary, Coalesce, TryCatch, Try, Return, Break, Throw, Spawn (#558, #125, E9 U8)
check: expect and expect_kept are Nikaia (C-5g, #558, #125)
check: made_from_text, a_name_in_text, may_fail_here, the reference at a call, gives_back (C-5f, #558, #125)
check: the walks below the core: overlap branches, guards inside boxes, claims, what a task keeps across a pause (C-5f, #558, #125)
emit: the places of the core are Nikaia: SafeMethod, Field, SafeField, Index, Cast, StructLit, With, Tuple, ListLit (#558, #125, E9 U7)
emit: the calls of the core are Nikaia: call, call_inner, method_call, the MethodCall arm (#558, #125, E9 U6)
emit: the arguments and the call of the core are Nikaia: args, called (#558, #125, E9 U5)
check: the core’s scaffolding (baton, core.rs, the hubs expr and block) and the pilot grammar_call, a_compound_write_to_a_map_slot, an_instantiation, a_mut_parameter_given_away (C-6, U-c0, #558, #125)
check: the leaves below the core: sequences asked for what they are not, cleanup moves, bounds, buffers, C entry points, members nothing writes down, arithmetic on declared types (C-5e, #558, #125)
check: task crossings, the number fold, binding and boundary, arms at null are Nikaia (C-5d, #558, #125)
emit: the match of the core is Nikaia: patterns, boxed parts, arm bodies, match over a sum
Expr::Match arm), pattern_written, arm_body_led and match_over_a_sum as emit_core_* of EmitState; the boxed parts a pattern looks inside are counted and written by place instead of handed back as references. The Rust match_expr is a one-line stub, the dead Rust is deleted. EmitHost gains copied_bindings and guard_copies.emit: the reads of the core are Nikaia: held receivers and indices, comparisons against a length, receivers, field calls, index reads, overlap, select
?. receivers, indices read from the target), against_a_length, receiver, a_field_call, index_read, overlap and select as emit_core_* of EmitState; the Rust methods are one-line stubs and the dead Rust is deleted. EmitHost gains overlaps_user_code.check: the number knot, kept lambdas and a few field readers are Nikaia (#558, #125, C-5c)
emit: the leaves of the core are Nikaia: keys, postfix bases, operands, strings, f-strings, options, guards, grammar entries, templates, blocks, ifs (#558, #125, E9 U2)
check: patterns and boxes, small part questions and the open numbers are Nikaia (#558, #125, C-5b)
tools/check_boxes.nika (what a pattern asks of a boxed part, NK1193, and the match that misses a case, NK1151; CheckDepth), check_parts.nika (a read extended by a field, a fold part that may pause, a field that holds code, the integer a number receiver takes) and check_numbers3.nika (the open numbers of a body decided by their uses, NK1200, NK1116) are CheckState methods; the Rust methods are one-line stubs and the dead ones are gone. No change to what a program means.The questions below the emitter’s core are Nikaia: the ?. holds, resize fills, the options of a call, what a catch guards, the keeps a statement declares (tools/emit_core_below.nika, #558, #125, E9 U1b).
emit_held_in (the receivers of ?. and whether holding each first is safe, returned as values and not threaded through &mut), emit_filled_at_once, options_of, caught_channel_is_named, caught_sum, branch_pauses, write_keep_prelude, keeping_callee, map_key, stays_the_callers, EmitWalk::reflected and emit_core_guard_reopening move to Nikaia; the Rust methods of those names are one-line stubs and held_in, branch_values, callee_name, keep_prelude and mentions_symbol are gone. EmitHost gains names_read (the names a guard reads, string holes included), the one thing the guard’s reopening needs from Rust. unreachable_patterns is allowed on tools for the else a one-pattern match over a struct must write. held_element stays Rust: it is a destructure and an adapter call, and a Vec[ref Expr] result cannot borrow from a walk handed in by closure. Output unchanged: package.rs is byte-identical under the previous compiler.check: the fallible and pausing walks, cleanups, reads and views below the core are Nikaia (#558, #125, C-5a)
tools/check_fallible.nika (a method call or a loop that can fail or pause: method_options, method_propagates, method_pauses, fallible_step, pausing_step, the walk of a failing sequence, seen_through, fits_through_deref, and the cleanup and teardown a type holds), check_reads.nika (a view compared with a value, a lookup key lent, a map read kept or assigned, a_read, the parts a mut parameter gave away, record_cast) and check_places.nika (a break with no loop, a door written wrong, the stamp a lock passes on, arguments_of, an_owned_place, the match over several error types) are CheckState methods; the Rust methods are one-line stubs and the dead ones are gone. No change to what a program means.The type group of the emitter is Nikaia: ty, ty_counted, the tethered wrapper and the extern pointer (tools/emit_core_types.nika, #558, #125, E9 U1a).
EmitState gains emit_core_ty, emit_core_ty_counted, emit_core_tether_wrapper and pointer_for; emit_settle_kind, emit_cleaned, emit_boxed_future and emit_counted_turn are the small words that depend on the build. The Rust methods of those names are one-line stubs, and written_name, a_list_read_as_a_slice and the free pointer_for of emit/mod.rs are gone. Nothing calls back into the Rust emitter, so EmitHost is unchanged. lower-std is the oracle: the rest of package.rs is identical.emit_deep_nesting.rs) lowers a type, a chain of casts and an index nested 500 deep through the moved paths.check: CheckHost and the seven functions that needed the compiler (#558, #125, C-4)
tools/check_host.nika declares trait CheckHost (a hole read back as an expression, the call a kept function stands for, a symbol, a build-time value evaluated, compiled and run, a file read) and moves build_time_value, compiled_build_time_value, unmarked_hole, a_build_time_run_that_stopped, a_lent_part_is_handed_on_as_it_is and a_file_this_build_may_not_read into CheckState as [H: CheckHost] methods; check/host.rs implements the trait over the parsed files and what the build may read. No change to what a program means.The emitter’s core has a host: EmitHost, the walk baton and three moved methods (tools/emit_host.nika, emit/host.rs, #558, #125, E9 U0b).
trait EmitHost (expr for now) is implemented by the Rust Emitter; EmitState gains emit_core_operand, emit_core_bare_range and emit_core_index_expr, and the Rust methods of those names are one-line stubs. The walk is a baton (w: RefCell<Option<EmitWalk>>), Flow converts to EmitFlow + EmitScope only at the crossing, a refusal raised by Rust code under a crossing is stashed and returned whole (it keeps its line), and Build::for_emit() fills EmitBuild. The unused_variables lint is allowed on tools for the parameters every core method takes alike. Output unchanged: package.rs is byte-identical under the previous compiler.A trait method’s mut parameter lowers to &mut T in the declaration (ADR-094 D3, #558, U0a).
walk: Walk while the impl wrote walk: &mut Walk; trait_method now writes the &mut too, so a mut parameter works through a trait end to end. No rule visible to programs changes. Test: tests/trait_mut_parameter.rs.check: the checker’s walk state is a Nikaia CheckFlow (#558, #125, C-3)
tools/check_flow.nika holds the 49 fields of the Rust Checker that were still Rust-typed (flags of the walk, method sets, read index, open numbers, unsigned slots) as CheckFlow, held by CheckState.flow; positions are i64, hash maps are ordered maps. No change to what a program means.EmitState holds the hand-over tables the core needs (#558, step E5b).
template_holes, template_errors, hole_wraps and kept_calls are tables of EmitState, filled by the Rust adapter from Parsed after the check, from the same caches, so the same expression ids come out (ADR-340 D2). A key that is not there is refused, never parsed again. Parsed gains the accessors cached_holes and cached_kept_calls. Output unchanged.The checker reads a file’s items through a view: ParsedView, and fifteen item-reading functions are Nikaia (tools/parsed_view.nika, tools/check_items.nika, #558, #125, C-2).
ParsedView { words, aliases, items, package } is what the checker reads of a parsed file. CheckWorld gains here (this file’s view), beside (the other files’ views), here_at (where here stands among them, or -1) and fields_by_base (built once per walk). newly stays with the Rust Checker: Newly is the public type of check and project.declares_a_type, declared_with_no_arithmetic, either_param, either_field, through_a_package_unread, a_package_where_a_grammar_stands, an_extern_struct_named, a_c_value, implements_error, a_placeholder, shape_of, a_function_nothing_declares, a_trait_nothing_declares, a_head_nothing_declares and types_that_contain_themselves, with held_inline, reaches and has_a_default. The Rust methods are one-line adapters.The emitter’s entry point, the program’s own main and the held structs are Nikaia (tools/emit_entry.nika, #558, step E8).
user_main, entry_point and held_structs are methods of EmitState that hand back the text; the Rust Emitter writes it. Output unchanged.The questions the emitter’s walk asks of the file and of its keeps are Nikaia (tools/emit_below.nika, tools/emit_keeps.nika, #558, step E7).
impl EmitState; so do the keeps a function declares and hands a call, a tethered let’s type and whether a held struct’s field carries a view. The Rust methods call them.A method’s function value is lent on, as a free function’s is (#558, #125).
fn parameter lowers it as &impl Fn, and its callers pass &, so a method can hand its function to itself in a loop (E0382 before). lends_in_own is lends_in for an own function or method; library methods keep taking it by value. check_fields.nika’s lent-parameter test and the emitter use it, and a_table_that_crosses, quoted and the prover’s methods take &impl Fn.The grammars, the keep plans and the methods are tables of EmitState (#558, step E5).
EmitState holds the grammars by name, the buffer plans (buffers.nika’s BufferPlan, as the Nikaia pass hands them back, with i64 places and KeepAt) and what each impl says about its methods, by Type::method and by the bare name where only one impl declares it. The Emitter no longer holds them.The emitter’s small kinds are Nikaia: Tail, Lifetimes, Pointer, Method, and the halves Flow splits into (tools/emit_kinds.nika, #558, step E6).
EmitTail, EmitLifetimes (the ten spellings and the ones derived from them), EmitPointer, EmitMethod and EmitNamed replace the Rust types of those names, which emit imports under the old names. EmitRefused (with impl Error), EmitBuild (the build’s settings as booleans), EmitFlow (the flags, Copy) and EmitScope (the owned text and lists) are declared for the move of the walk; Rust’s Flow is not converted yet.A field in a struct pattern takes a pattern (ADR-291 D25, #573).
Point { x: 0, y }, Move { x: px, .. }, Shape::Circle { center: Point { x, .. }, .. } and E::Bin { op: Op::Add, l, r } parse, check and lower as Rust writes them; field alone is field: field. MatchPattern::Named holds fields: Vec[PatternField] (a name and a pattern) in place of the names. The type of each part is read from the field’s declared type, the binding-set rule (NK1155) and the lending of a part only read apply to the sub-pattern, and the emitter writes field: pattern.The checker’s walk holds no address, no i128, no closure boundary and no lazy cell (#558, #125, C-1).
open_calls is keyed by a node id (the call’s first argument), not an address; this file’s place among beside is an index, not a pointer comparison; Literals and literal_receivers hold Integer, not i128; a walk past a boundary is enter_boundary / leave_boundary, not a closure; fields_by_base is built once with the walk; barrier, reaching, reached and UnsignedSlot::into are String.A lent parameter, a map’s first write, a comptime asked for a type and a field holding code are Nikaia (tools/check_fields.nika, #558, #125).
a_lent_parameter, a_map_first_written, comptimes_asked, a_field_holds_code and fields_seen are methods of CheckState; current and open_comptimes moved from the Checker into it. Left in Rust: grown_text, a_kept_lambda_takes_these, a_kept_lambdas_hulls and walks_the_fields_of (they walk a block through the emitter’s visit_block), hands_over (it reads read_index).The checker’s method and stored-code tables and the grammar pattern types are Nikaia (tools/check_state.nika, tools/check_grammar.nika, #558, #125).
MethodCalls, CallsInATask and StoredCode are Nikaia structs, and methods, stored_code and stored_frames live in CheckState: the Checker no longer holds a Checked. pattern_type and bindings_of (the type a grammar pattern’s result has, and the names a rule’s pattern binds) are methods of CheckState; rule_results moved into it, and a binding made there takes its id from a counter of its own.The emitter’s questions about calls, patterns and shapes are methods of EmitState (tools/emit_exprs.nika, emit_misc.nika, #558, step E4).
emit_exprs: whether an argument is code the callee runs or a place it changes, what a plain value is wrapped in to fit its slot (emit_around, emit_around_value), whether an expression is written as a cast, which index is a slice, which struct is built by its new, and whether an expression is never negative (a method of EmitWalk). emit_misc: whether a method call pauses or can fail, whether a call closes a pausing cycle, the boxed bindings of a pattern, a package’s grammar entry, what a shape walk unrolls, the Rust of a declared handle type, the names Rust reserves, a narrowing cast, a code parameter handed on whole and an attribute in Rust.The error sums and what the emitter asks of a type are methods of EmitState (tools/emit_sums.nika, emit_types.nika, #558, step E4).
EmitState answers which error names the emitter can write, writes the generated sums (ADR-280 D15) and names a function’s failure channel; which members of a variant are boxed, which types carry a view, which extern types are handles, what a type’s fields default to and which arguments of a callee are shared handles. The Emitter methods of those names pass world and read the result.The emitter’s state is three Nikaia structs (tools/emit_state.nika, #558, step E3).
EmitWorld (the file’s names, aliases and items, and std’s ledger), EmitState (the checker’s CheckFacts, the ledgers and the tables derived from the items, read-only from the first line written) and EmitWalk (the 19 Cells and RefCells the walk writes to, as plain fields). The Rust Emitter holds s, w: RefCell<EmitWalk> and world; every access to the walk goes through walk and walking, which end the borrow when the closure returns. Tables keyed by a Symbol are keyed by its name (Symbol is not Ord). The parsed file, the build’s settings, the grammars, the keep plans and the method table stay Rust fields.Two number types in one operation, array literals and collect by use are Nikaia (tools/check_numbers2.nika, #558, #125).
two_number_types (NK1199), two_integer_types_compared, a_compound_over_two_integer_types (NK1105), widen_operands, widen_operand, widened_elements, array_literal (NK1157) and collect_by_use write CheckState.facts directly.Field reads through ?., a ?. or ?? over a value never absent, and text built where String is wanted are Nikaia (tools/check_fields.nika, tools/check_texts.nika, #558, #125).
a_safe_field, reaches_through_a_plain_value (NK1121, NK1217), nothing_to_fall_back_from (NK1211, NK1216) and a_fallback_that_owns_what_the_left_side_views (NK1185) write CheckState.facts directly. text_literal, tuple_literal, list_of_tuple_literals, some_at_the_tails, ends_in_text, ends_in_null and a_byte build text and bytes where a position asks for them.The checker’s facts are one Nikaia struct, CheckFacts, the emitter reads (#558, #125).
Checked is {findings, facts, methods, stored_code}: the 109 tables are CheckFacts (tools/check_facts.nika), filled as CheckState.facts, keyed by i64 (value_node is an i64). Propagation is gone: propagation_against returns CheckFacts. The emitter holds one facts field in place of 102 copies; bounds::Sites and the proofs’ sets are i64. Wrap, KeyForm, Viewed, Narrowing, PausingEntry and Claim are the Fact* types of the struct, with owned strings. held_across_a_pause stays in the struct: five tests read it.The emitter’s output buffer and its error-name questions are Nikaia (tools/emit_out.nika, emit_items.nika, #558, step E2).
EmitOut (the buffer and the entries of the source map: start, end, span, unit) with push, mark, record, append and shift_entries; Out in emit is that type, and SourceMap is built from its entries once, when a module is done. take_lend and insert_after_first_brace stay Rust (std has no String::pop or insert_str entry). error_kind, member_of and sum_member_type read the declared errors and the library ledger.Out records a span with a mark and a record, not a closure (emit, #558, step E1).
Out::from and Out::scratch took a closure; every site now writes let st = out.mark(); <body>?; out.record(span, st) (a span is recorded only after the body succeeded, never truncated on an error). The six sites that reset specialising keep the order: reset, ?, record. Nothing a program means changes; the emitter’s output and source map are the same. Prepares Out as a Nikaia struct (a closure that captures the emitter is not expressible).CheckFacts is declared in Nikaia (tools/check_facts.nika, #558, #125).
i64), with FactWrap, FactKeyForm, FactViewed, FactNarrowing, FactPausing and FactClaim; CheckFacts::empty(). Not used yet: the next package makes it Checked.facts. The dead table held_across_a_pause is not in it.A type variable two integer types are handed to, and a build-time run that stopped, are reported from Nikaia (tools/check_widen.nika, tools/check_reports.nika, ADR-285 D32, ADR-321 D1, #558, #125).
one_type_variable_two_integer_types (NK1102: the loop over the arguments and the report) and the report of NK1152 (a_build_time_run_that_stopped_at) are methods of CheckState. Reading the host’s text of where a run stopped, and the name of a function in another file, stay in Rust.What the contracts say about a call the emitter writes is Nikaia (tools/emit_calls.nika, #558, #125).
pausing_key (an .await is owed), library_pauses, pauses, can_fail (a ? is owed), callee_name, joined_throws (the errors a set of branches throws together) and returns_either read the unit’s own ledger, its constructors’, std’s and a described crate’s in the order the chain asks. The Emitter methods of those names pass their ledgers and the interner.A computed value as the program holds it, and eight reports about one expression, are Nikaia (tools/check_consts.nika, tools/check_exprs.nika, ADR-311, ADR-152 D4, #558, #125).
rust_value and rust_array_type (a build-time value as a const’s Rust), and crosses_as_fixed (whether a computed list or text crosses as the fixed type its declaration names; CheckCrossing) are Nikaia. written_below, unwritable_in and unwritable_payload (a value written against its declaration, and what a const cannot hold) and a_table_that_crosses (the Fixed::new(…) of a table; CheckTable) are Nikaia too. The float spelling and a variant’s declared payload (the host’s item tree) are callbacks. NK1157 is reported from crosses_as_fixed directly.NK1215, NK1197, NK1201, NK1206, NK1185, NK1105 (text of its own given to a name bound to a literal) and NK1108 (expect_bool), and the three sentences of a view kept where text of its own is wanted (CheckKept) are methods of CheckState, said in the expression’s own words (written_expression over the file’s names).The emitter’s pausing reach and its never-negative locals are Nikaia (tools/emit_reach.nika, emit_locals.nika, #558, #125).
emit_reach::pausing_reach: the call graph of a unit’s functions and methods restricted to the calls that may pause, and its closure (the futures that reach themselves, which the emitter boxes). It reads the edges off emit_walk::visited_in_block.emit_locals::nonnegative_locals: the lets of a literal and the fors over a range from a literal that are declared once, assigned only literals added or multiplied, and never handed to a call. It records each statement it has seen by where it stands, its kind and the id of what it holds, so every_statement, collect_blocks and statement_key are gone from emit. nonnegative_names takes the Parsed (the symbols are its interner’s); bounds passes it.The emitter’s first leaves are Nikaia: operators, text, items and the walk (tools/emit_ops.nika, emit_text.nika, emit_items.nika, emit_walk.nika, #558, #125).
emit_ops: precedence, binary_op, unary_op, repeat_suffix, pointing, jumps, jumps_at_the_end, a_number, only_literals, a_negation_inside, is_length, is_count, a_list_read_as_a_slice, frame_attribute, is_a_place, binds_a_part. emit_text: rust_string, rust_format_escape, top_level_parts, format_of, truncating, checked, held_literal, plain_rust_spelling. emit_items: foreign_params, opaque_handles, foreign_results, foreign_traits, declared_errors, sum_name, sum_variant, variant_slot, dsl_name.visit_expr and visit_block ask emit_walk::visited_in_expr and visited_in_block, which return the expressions in the order the walk meets them; the Rust callers keep their closures. template_holes_bound is Nikaia too. holds_view is views::holds_view.Emitter and its methods, Out, Flow, pausing_reach and the walks that capture state.Six reports of the checker are Nikaia (tools/check_reports.nika, #558, #125).
NK1164 (a bound the argument’s type does not meet), NK1138 and NK1139 (a change to a binding not declared mut), NK1178 (a grammar the build could not run; CheckWall is what the host reports), NK1184, NK1101 and NK1123 (a shared handle made of one value) and NK1117 (a std name without its module) are methods of CheckState. changed and said_mut moved from the Checker into it.Which integer type holds both, where a value is widened and where text stands for a file’s name are Nikaia (tools/check_widen.nika, ADR-285 D32, ADR-319, #558, #125).
common_integer, widened_into, unsigned_from_signed, an_integer_that_does_not_widen, number_where_no_number_is, names_a_path, text_as_a_path, views_into_text, views_into_nullable_text and binary_op_text, and the reports of NK1102 (a type variable two integer types are handed to), NK1157 and NK1200 are Nikaia. The checker keeps thin adapters where a result is a &'static str.What was taken, what was read after and what a loop or a lambda takes again are Nikaia (tools/check_takes.nika, ADR-293, ADR-094 D7, #558, #125).
taken, first_read_after, hands_over_path and hands_over_name, a_sequence_is_walked and a_sequence_is_taken, the end-of-body reports of NK2101, NK2105, NK2702 and NK2108 (a_task_took_what_is_used_again, data_handed_over_is_used_again, a_sequence_was_walked_twice, a_part_left_out, a_part_is_missing), a_way_out, taken_so_far, a_branch_that_leaves, lent_here and a_lent_let are methods of CheckState. The fourteen fields they read (walked, handed, reads_on_paths, written_at, read_at, parts_out, gives, ways_out, branch, lent_lets, moved_into_a_task, task_bindings, reading_only, read_seq) moved from the Checker into CheckState.Checked::unsigned_literals is keyed on the literal’s id alone (ADR-340 D5); the value was a second key for the same node. The emitter reads it the same way.parts_taken_out_of_a_mut_parameter (it tells the emitter) and the walks that call these.A field in a struct pattern takes a pattern (ADR-291 D25, #573).
Point { x: 0, y } tests a field, Move { x: px, .. } binds it to another name, Shape::Circle { center: Point { x, .. }, .. } reaches inside; x alone is x: x (Part I 3.4). Today the parser still refuses the :.No table is keyed on the address of anything: a match arm’s facts are keyed on its body’s id, a ?. reach on its receiver’s (ADR-340 D4, #558, step 4).
lent_bindings, taking_arms, guards_inside_boxes and copied_bindings were keyed on the address of the arm’s MatchPattern; they are keyed on the id of the arm’s body (an arm has exactly one), so match_pattern and two checker functions take the body. flattened_reaches, copied_reaches, viewed_reaches, lent_reaches and held_reaches (x?.f, x?.m()) were keyed on a statement and a name; they are keyed on the id of the receiver the ?. reaches through.every_statement, the walk of nonnegative_locals, told a statement it had seen by its address; it is told by where it stands, its kind and the id of what it holds.nullable_fields is keyed on the id of the literal and the field’s name: a field written as a bare name has no value of its own to carry an id. The call a named function stands for where a function value is kept is built once per name and arity (Parsed::kept_function_call), so the checker and the emitter look the same nodes up.grep -c 'as \*const' on check and emit: 0.argument_shape is gone: the call arguments, the struct-literal fields, the asserts and the tier pass’s hole wraps are keyed on the id (ADR-340 D4, #558, step 4).
lent_args, mut_args, copied_args, nullable_args, nullable_fields, lookup_keys, count_args, path_methods and field_calls were keyed on a statement, a callee and a position and held the {:?} text of the argument; they are sets and maps of ids. a_lent_part_is_handed_on_as_it_is removes the ids of the lent part’s reads instead of the shapes in a statement range.claims and the prover’s held are keyed on the statement and the id of the condition; tools/prover_*.nika lose the shape_of callback and use node_key.Parsed::hole_wraps and wrap_hole name the value by its id, so wrapping a hole is one walk. check::argument_shape is deleted: no table is keyed on the text of an expression any more.call_on takes the receiver; counts_in_usize the arguments.The checker’s tables about an expression, kept by statement and the text of its shape, are keyed on its id (ADR-340 D4, #558, step 4).
Checked (owned_copies, boxed_reads, flat_reads, copied_reads, text_as_is, lent_coalesces, view_coalesces, kept_lambdas, pausing_lambdas, map_keys, …) were keyed on (the byte the statement starts at, argument_shape(expr)), the {:?} text of the expression: two equal expressions of one statement shared an entry, and a copy made by one reader and another by the next agreed only by their text. They are keyed on value_node(expr), the id; (usize, String) is usize.CheckPendingCoalesce.key is the ??’s id. Seven functions that took a span, a statement or a flow only to build the old key lose the parameter.claims, which keeps its statement for the line it reports.The key at stood for on a list, a text literal and a with is the node’s id (ADR-340 D4, #558, step 4).
Checked::array_literals, owned_texts, view_fallbacks and with_types are keyed on value_node, the id. ListLit and LitStr lose their at; With keeps its at as the place a message points at.check::text_key, the hash of a statement, a hole’s text and an offset that told a literal inside an f"…" hole from one in the file, and the hole state both the checker and the emitter kept for it, are gone.Parsed::hole takes the id of the template it is a hole of: one template’s hole is parsed once and read the same by every reader, and the same text in another template has nodes of its own.The host callbacks node_of are gone: the tier pass and the bounds walks name a node by its id (ADR-340, #558, step 5).
tools/text_tiers.nika, bounds_basic.nika and bounds_walk.nika no longer take node_of: fn(ref Expr) -> i64 sync; they key their sets on node_key (id_of as an i64, tools/ast.nika), which the emitter reads again from the same tree or from a copy of it.The checker and the emitter key their tables about an expression on its id, not its address (ADR-340 D4, #558, step 4).
expr as *const Expr as usize (13 in check, 9 in emit) and the checker’s address helper are value_node, the id; the one std::ptr::eq on two expressions compares ids. Checked’s wrapped, some_tails, held and the read sites no longer depend on where the allocator put a copy.MatchPattern and a Stmt, which carry no id (ADR-340 D6 leaves them open).A node the compiler builds takes a new id, a clone keeps its own (ADR-340 D2, #558, step 3).
Expr the checker, the text-tier pass or the folds of tools/tree_build.nika build is given fresh_id(); a node that is the same node after a rewrite keeps its id (grouped marks a Binary and does not rebuild its id).xs[a..] holds xs as the base and as the receiver of len) keeps its id where it is first met and takes a new one at the second place: refine separates repeated ids before the tier pass reads them.expr_ids.rs - a rewritten tree has no two nodes with one id, a wrapped hole keeps its ids and the wrapper takes a new one, a cloned subtree has the ids of the original.Every Expr node carries an id the parser gave it (ADR-340, #558, step 2).
Expr ends in a NodeId (nikaia_std::node_id): a u32 the parser counts up in the order it makes its nodes, unique within one parsed file. id_of answers the number. The equality of two ids is always true, so two trees that differ only in their ids are equal (D3) and Expr keeps its derived PartialEq.NodeId::fresh(), a process-wide counter from 2^31 up (D2). A hole of an f"..." parsed after the file takes the next numbers the file has not given (Parsed::hole).tools/tree_build.nika’s folds take the first of the numbers the parser set aside for them. NodeId prints as NodeId in {:?} (the checker’s argument_shape is that text) and with its number in {:#?}.check::value_node, the key of most tables about an expression, and the text-tier pass’s wraps (wrap_item, wrap_hole) are keyed on the id instead of the address; a copy made by one reader and another made by the next now agree. The other address keys follow. Tests: expr_ids.rs.The checker’s refusals about pauses, locks and build-time runs are Nikaia
comptime the build may not run moved into tools/check_pauses.nika, check_doors.nika and check_buildtime2.nika (#558, #125).The checker’s state, its scope and the refusals that need only these are Nikaia (#558, #125).
Checker holds a CheckState (tools/check_state.nika): the findings, the scope and what the program declares. A Finding is Nikaia’s CheckFinding, so its code is text, and CodeOps::code_str gives the &'static str the tables and tests compare.tools/check_*.nika: lookups of what a name stands for, a pattern’s bindings, whether a value is taken away or copied, the refusals about names, imports, tables, build-time values, with and catch. Their order and wording are unchanged.NK2302 knows a subject declared in another file of the package from the ledger: a call on a struct that holds no view keeps nothing. Test: views_kept.rs.The checker’s state begins to be Nikaia (#558, #125).
Checker holds while it walks a body - a name in scope, what was taken or read of a path, a way out, a body that runs again, what a catch guarded - are declared in tools/check_state.nika with the questions each answers on its own (overlaps, revives, apart, a loop no jump leaves). Their positions are i64 (ADR-294 D7), their words String.tools/check_walk.nika (the walk of contracts::sync, in Nikaia).develop is green under clippy again.
tools package allows nine more clippy lints about code the
lowering writes (assign_op_pattern, single_char_add_str, useless_vec, …).lower-std reports every refusal of a file (#575).
tools/ or a module of std with ten findings cost ten cycles, because only the first was said. Every refusal is now reported, in file order, as nikaia check says them, followed by their count. Test: sysroot.rs.Four lowering defects the self-hosting sessions found (#125, #567, #570, #574).
match over a let over a place (let v = vs[0], then match v { V::Case(a, b) => list[a] }) is read as the number: it was a &i64 below (#567).v[v.len() - 1] = x and v[v.len() - 1] += 1 were rustc’s E0502 (#570).x ?? BinaryOp::Add for an x: ref BinaryOp? lends the fallback too, a constant promoted to a &'static: the answer is a view whichever side gives it, and the value was a mismatch (#574). The lowered tools package changes by one line.selfhosting_defects.rs.A field moved out of a borrowed value is refused wherever the value is lent, not only for ref self (NK1131, #576).
return l.ty out of a ref parameter, a for binding over a list or a let over a place, and the same field as a body’s last expression (also for ref self), passed the check and reached rustc with cannot move out of l.ty which is behind a shared reference. They are NK1131 now, naming the whole path, with .clone() and a result declared ref T as the ways out. Part III’s table says which subjects. Test: borrowed_part.rs.An arm that takes parts of a mut parameter and writes the whole back lowers (ADR-094 D7, #576).
match over a mut parameter is a match over a &mut below, so the arms bound references and rustc refused e = E::Add(l, r) in words about a file nobody wrote. An arm that takes a part out of the parameter now works on the value taken out of the place (std::mem::replace with a variant that costs nothing to make), binds the parts by value and writes the whole back with its assignment; an arm that takes nothing reads a number it binds as the number, and writes no ref where the pattern borrows by itself. The place may be a field of the parameter. Test: selfhosting_defects.rs.NK2108 is built: a part taken out of a mut parameter is given back before the function is left (ADR-094 D7, #576).
mut parameter, which parts an arm of a match over it took out (handed to something that keeps it). A return, a throw or the end of the function with a part still out is NK2108, and so is a call that can fail between the taking and the assignment that gives it back; the assignment has to stand on every path from the taking to that point. Naming the part, where it was taken and the way out. Test: mut_parameter_parts.rs.A part taken out of a mut parameter is given back before the function is left (ADR-094 D7, #576).
mut parameter (and ref mut self) is whole again, and no throw or failing call stands between taking a part and giving it back. Both are NK2108 (Part I 6.5, Part III). The checker today still accepts a function that takes a part and keeps it; rustc refuses the result.A mut parameter is assigned and read through its reference (#576).
mut parameter is a &mut T below, and n = n + 1 was written as it stood: rustc refused the very program NK1138 asks for. Assigning one whole now writes through it (*n = …), a number, bool or char read out of one is read as the value (*n), and one handed to another mut parameter is handed on as it is. Test: selfhosting_defects.rs.A mixed String reassigned from a view of itself lowers (#563).
rest = rest.strip_prefix(">") ?? "" into a String that text tiers made EitherText borrowed from the buffer being replaced (rustc E0506); the value is now copied first. The lowered tools package also lacked IntoEither in scope (E0599); nikaia-std imports it. Test: selfhosting_defects.rs.More of the toolchain is Nikaia (ADR-294, #125).
bounds: what the bindings of a grammar’s alternative matched, and what a
callee’s ledger entry ensures, are tools/bounds_matched.nika.proofs: nikaia.proofs as text - its lines read back and the file a book
renders - is tools/proofs_book.nika.The tools package no longer warns on valid shapes (#565).
let mut s = init followed by branches that always assign s, and while true { … return … } followed by a trailing return, gave unused_assignments and unreachable_code in the lowered tools package. Both are allowed over the module, beside the lints it already allows. Test: selfhosting_defects.rs.A constructor call of a struct another file of the package declares lowers (#560).
Spanned(x, span) in a tools/*.nika file was lowered to Spanned(x, span), which is no Rust: the emitter knew only the structs of the file it lowered. The structs of the files beside it count too (one namespace, Part I 9.1), so the call is Spanned::new(x, span), as NK1149 steers. Test: selfhosting_defects.rs.drain() over a mut parameter takes the elements (#564).
for x in v.drain() over a mut parameter (or a field of one) lowered to into_iter() on a &mut, which handed out references and left the caller’s list full. It is now drain(..). Test: selfhosting_defects.rs.More of the toolchain is Nikaia (ADR-294, #125).
build_time: the evaluator of a build-time expression - calls, loops, f"…",
structs and variants, a grammar run on a text - is tools/build_eval.nika. It
asks the compiler for the program’s files, the ledger, what a build may read
and the grammar workshop. A refusal’s because, why and way_out are
Strings now.A ?? whose fallback is an index read lowers to a value (#561).
opt ?? list[0] was lowered to *get(..).into(), whose * binds looser than the .into(), so rustc could not infer the target (E0283). The fallback is now cloned out of the place, for text and for numbers. Test: selfhosting_defects.rs.A view cut from a call’s result no longer outlives it (#559).
let v = f().trim_start() bound a view of a temporary String dropped at the end of the statement (rustc’s E0716). The lowering binds the call’s result to a hidden local first whenever a text view method is in the chain. Test: selfhosting_defects.rs.More of the toolchain is Nikaia (ADR-294, #125).
comptime_run: the main of a build-time sub-program, the map from its
lines back to bytes of the file, and the bundle it is linked against are
tools/comptime_driver.nika. Compiling and running stay Rust.The grammar language can say what the compiler’s own grammar says (ADR-339, #557).
span(p) and here give a match’s byte range and a position; same_line
fails where a line break precedes; /// is a rule, and a grammar declares
no state and no extern rule. Part II 10.8.More of the toolchain is Nikaia (ADR-294, #125).
grammar_run: what a grammar run at build time prints is read back by
tools/grammar_decode.nika, which also holds the text the sub-program is made
of - the encoder’s helper, its main and its manifest. Compiling and running
the sub-project stay Rust.More of the toolchain is Nikaia (ADR-294, #125).
tools/logic_*.nika: the linear
solver and the checker, the normal form and the text of certificates and
models, SMT-LIB 2 and Alethe. The nikaia-logic crate is gone; the solver
is asked about the prover’s own TermArena, so the copy of the terms it
needed is gone with it. What it answers is what it answered, certificate for
certificate.More of the toolchain is Nikaia (ADR-294, #125).
views: the fields each declared type holds and the names a method carries into its input are tools/views.nika.More of the toolchain is Nikaia (ADR-294, #125).
contracts::tether: --tethers - the report of what the analysis solved and where each buffer lives - is tools/tether.nika.More of the toolchain is Nikaia (ADR-294, #125).
parser: the aliases a file’s use items declare, and the rewriting of a
file’s declared &[T] as Vec[T] for a grammar run, are
tools/parsed_tree.nika.More of the toolchain is Nikaia (ADR-294, #125).
contracts: the ledger’s item loop - what an extern block and a grammar enter, the types a file declares, a build-time value as a literal, and an expression function’s ensures - is tools/ledger_items.nika.More of the toolchain is Nikaia
parser: the postfix steps, the folds of a head and its tails, a lambda’s
parameters and a guarded jump - what the grammar’s actions build the tree
with - are tools/tree_build.nika (ADR-294, #125).More of the toolchain is Nikaia (ADR-294, #125).
diagnostics: a relayed rustc message - its layout, the internal error and the message nothing maps - is tools/render.nika.More of the toolchain is Nikaia
parser: a parse error in the reader’s words - what was expected, the mistake
where it was made, the notes a refusal gets - is tools/parse_errors.nika
(ADR-294, #125).More of the toolchain is Nikaia (ADR-294, #125).
cexport: a package as a library C calls is tools/cexport*.nika - the
entry points read off the declarations, the wrappers and the header, the
ctypes module of nikaia bind python and the N-API module of
nikaia bind node. Reading the program’s files and handing them over stay
Rust.More of the toolchain is Nikaia (ADR-294, #125).
contracts: an enum’s cases and a struct’s fields, as the ledger records them, are tools/declared.nika.More of the toolchain is Nikaia
parser: a number as the grammar reads it - the four spellings, the separator,
a scale, and every refusal - is tools/parse_numbers.nika; the interpreter
nikaia interpret is tools/interpreter.nika (ADR-294, #125).More of the toolchain is Nikaia (ADR-294, #125).
contracts::trust: which maps are keyed by what the program wrote itself (ADR-010 D2) is tools/trust.nika. Reading the ledger’s sources stays Rust.More of the toolchain is Nikaia (ADR-294, #125).
libraries: which of pkg-config’s words reach the linker, and the flags as
rustc takes them, are tools/libraries.nika. Running pkg-config and the
sentences of NK1224, NK1225 and NK1226 stay Rust.develop is green under failing closed (ADR-286 D41, #497).
c::Id in a package that calls deep c.std’s is:
http::Method::Post.fs::Mapped, Shared) answers methods and indexes with
what it holds: data.lines(), data[0..<7], counts.len().dsl block for a language no grammar is, an entry a
grammar does not have.More of what failing closed refused is typed, or asked where it belongs (ADR-286 D41, #497).
h.seen() on a Holder[i64]); Vec() whose first
push is a number holds one open number; x ?? "none" over a T? nothing
typed is the fallback’s type; a lambda in a list of a declared fn type takes
it; a dsl statement with :name holes is its text; full() on a type with
impl Error; a Seen[T] answers T’s methods; a select arm of no type
does not unsettle the others.hand(7), Holder { value: 21 },
a task’s join(), xs ?? 0); asset("…"), by its comptime; a DSL
driver’s Self::dsl, at each call site.NK1245 waits for the other refusals (ADR-286 D41, #497).
NK1245 is not said: a value another
refusal left without a type (a name never declared, a member a type does not
have, a jump that goes nowhere) is that refusal’s, wherever it is used.An expression whose type is not known is refused: NK1245 (ADR-286 D41,
#497).
comptime without a type (their uses type them), of an operator, branch,
block or jump, of a panic, of a reflected field’s of in a generic body,
or again where a part inside was refused.develop is red with this: the sites that are still unknown, and the tests
that expected silence, are listed on #497 with what to do for each.A lambda of a declared type, a field through Shared, dec[f64], a choice
of rules and an if beside a literal are typed (#497).
fn type is declared is of that type, not
only its parameters.deref, as Shared)
is the field of what it holds: db.host on a Shared[C] is C’s.dec[f64](…) as an f64 (Part II 10.8 reads into any
number type), and a choice whose alternatives agree, s:(ONE | TWO), as
their type.if at >= 2 { at - 2 } else { 0 } is the typed arm’s type, and the literal
arm is a use of it.Four more names the checker can type (#497).
return, break or continue comes to nothing:
let x = if c { return 1 } else { 2 } is the other branch’s open number.E::Add(E::Num(x), E::Num(y)) binds two i64s, E::Add(E::Neg { inner }, _)
an E.return
against a declared fn(i64) -> i64, a field of that type and an annotated
let type them, as a callee’s signature types an argument’s.for n in [1, 2, 3], and for y in ys over a list written in numbers, bind
the elements’ open number (ADR-285 D34).NK1183 no longer calls a number bound by a for a view of text.Three places the checker knew a type and kept it unknown (#497).
let
taken = if c { 1 } else { 0 }, and a match whose arms come to numbers, are
open numbers, decided by their uses and written into the generated let. A
branch that leaves is no value. A use no number can be is refused at it.for k in 0..<55 binds an open number of its own, given the two ends, where
it was unknown.match pattern binds the whole value with its type
(other => other), unless the name is one of the type’s variants.std’s lowered tools now write let mut taken: i64 and let cost: i64.What is written down is what can be called, and an error of several is an
Error (ADR-286 D36, ADR-280 D30, #509, #507, #512).
NK1171, also where Rust has one of that name:
String::from("x"), "x".as_str(), fs::frobnicate(…), a method a
program’s own struct does not declare, a function a crate’s description
does not list. The help names Nikaia’s spelling ("x", nothing, != null)
or the nearest name. A type parameter, a head only a use names, a foreign
name, a type seen through (deref) and a type implementing a trait of
another ledger are not claimed; Error’s message and full are.is_some and is_none on a T? (NK1125) are answered with != null and
== null.std writes down Vec::first, Vec::last, collections::HashMap::clear,
io::IoError::message and io::IoError::full; io::IoError has its
message in Rust too.error is an Error, one of
these: message() and full() are Strings, f"{error}" and
throw error pass, an arm of a match reads its member’s parts typed, and a
field or method of one member read outside the match is NK1107 or
NK1171. A value refused is said as an error, one of ConfigError,
io::IoError. The generated sum has a message().examples/k-nucleotide writes starts_with(ref wanted) for
wanted.as_str(), and the measurements fixture neg != null for
neg.is_some().Text becomes a number by a function per type (ADR-285 D35, #504).
std::text has parse_i32, parse_u8, parse_u32, parse_u64 and
parse_f64 beside parse_i64, each a T?: null where the text writes no
number or one the type does not hold. An unsigned type takes no -, not even
-0. A float is digits, an optional . and digits, an optional exponent, or
inf, -inf or nan in any case; .5, 1. and infinity are null.parse_f64 checks the shape in Rust (text_float.rs) and hands the reading
to Rust’s correctly rounding conversion; the integers are written in
Nikaia (text.nika).parse_f32 waits for f32 (#552)..parse() calls in the examples and benches are now
text::parse_i32(…) ?? N (parse_i64 in jumps).A list written in numbers has one open number for its elements (ADR-285 D34, #513).
let xs = [12, 14, 13] gives its elements one open number, decided as a
let’s is: by the uses - a parameter Vec[T] it is handed to (or a
.clone() of it), an annotated let, an element read into a typed place, a
typed value pushed or written into it, an element in a typed operation - else
the first type that holds every element, i32, else i64.xs[i] is a use of it as a name is: let s: String = xs[0] is
NK1103, two uses asking two types are NK1200, and xs[0] + xs[1] over an
immutable list of two 2000000000s is NK1116 where it stopped at run time.let
(let xs: Vec<u8> = vec![…]); a list no use asked for is written bare (D27).An open number where no number fits is refused at that use (ADR-285 D23, #554).
let n = 12 then let s: String = n is NK1103, where it reached rustc
as mismatched types: a use that asks an open number for text, a bool, a
char, a tuple, a container or a declared struct or enum is refused there,
the value named a number. An argument is NK1102, a field NK1106, a
return NK1104.let x: u8 = n passes.A supervisor’s child takes a policy: policy: and supervisor::Restart
(ADR-328 D5, D9, #95).
supervisor::Restart, supervisor::Next and supervisor::Failures are
declared in std’s Nikaia half (src/supervisor.nika), and their ledger
entries are derived from it. The supervisor’s runtime is
src/supervision.rs, which includes what that file lowers to.supervisor::child(…; policy: …) takes any value whose type implements
supervisor::Restart; std’s default is supervisor::Backoff(). The
supervisor keeps each child’s history - how long the attempt ran, its
failures, how long since the first - and asks the child’s policy at every
restart. Backoff starts a new cycle after a healthy run by itself.SharedMut::supervised(fn { … }; policy: …) takes the same policy; one
that gives up leaves the value poisoned.supervisor::Restart::decide’s self is supervisor::Restart.A task in task::scope may pause, and the scope polls it: built
(ADR-328 D10, #95).
NK2102 is retired);
NK2107 and the lock rules stay. A scope with a pausing task pauses itself,
and so does the function around it.std::task::scope_paused and scope_paused_on_pool: the scope’s function
and every pausing task are polled together inside the scope’s own future,
never handed to an executor, so a task may borrow and two tasks that wait on
each other complete. A task that never pauses runs where it is started at
no, on the pool at yes. A crash drops the running tasks, so their
cleanups run, and the scope throws task::Crashed.async block that names it, awaited in the block that owns it; a pausing
task is s.spawn_paused(async || …).A C library’s error codes are built: std’s below 100 000, its own from
100 000 (ADR-284 D33, #88).
100 000.std’s errors is exported: io::IoError’s
NotFound, PermissionDenied and NotText are the target’s ENOENT,
EACCES and EILSEQ (Linux’s on the Linux targets, WASI’s under
WebAssembly), Outside and Other 1000 and 1001; task::Crashed,
supervisor::Escalated, cleanup::Failure and Overtaken 1100 to 1400.
The header writes each as <PACKAGE>_E_IO_NOT_FOUND and so on.nikaia bind python raises FileNotFoundError for ENOENT and
PermissionError for EACCES, each also the package’s Error, with
errno set; nikaia bind node gives an errno status code 'ENOENT' and
errno its number.pub extern enum is built: a tagged union, by value (ADR-284 D32, #88).
extern before enum; the tree’s Item::Enum has
is_extern. The checker refuses one that is not pub (NK1237) and a
variant’s field that is no C value (NK1145); an extern enum is a C value
itself, so it may be a field of an extern struct or enum. The emitter
writes #[repr(C)] on it.<prefix>_<Type>_Tag and a typedef struct of the tag and
an anonymous union with a struct per variant that has fields, its member the
variant’s name in lower case, a positional field _0, _1, ….extern struct does: in and out by value, a run in and out,
a method’s self as const T* or T*, a field. A tag that names no
variant is E_ARGUMENT.nikaia bind python makes it a ctypes.Structure with the union, each tag a
class attribute; nikaia bind node an object { kind: "Variant", … }.A handle to an enum with payload is read through _kind and a getter per
field (ADR-284 D31, #88).
pub enum with payload that an entry point names is a handle, freed by
_free. <prefix>_<Type>_kind writes its variant as the C enum
<prefix>_<Type>_Kind (<PREFIX>_<TYPE>_<VARIANT>), and
<prefix>_<Type>_<Variant>_<field> reads a field, a positional one numbered
from 0. On a handle holding another variant a getter is E_ARGUMENT, its
out-parameter untouched, and last_error names the variant it holds.IntEnum and the getters properties;
the Node binding a frozen object and getters.A C library’s absent number is built: the value, then bool *present
(ADR-284 D30, #88).
T? of a number or of
a pub extern struct is exported with the value’s out-parameter followed by
bool *present; the status is OK either way, and null leaves the value’s
out-parameter as it was. The _async form forwards both.nikaia bind python makes null None, nikaia bind node makes it
null, in the Promise form too.A C library’s error codes: std’s below 100 000, its own from 100 000
(ADR-284 D33, #88).
std error that is an errno has the number of the
target the library is built for (NotFound is ENOENT, NotText is
EILSEQ); std’s other errors have fixed numbers from 1000, a block per
type; the library’s own variants are numbered from 100 000, no longer from
1. A function that throws several types has the codes of each. Part III
15.1 holds the table.An enum with payload at a C library’s boundary (ADR-284 D31-D32, #88).
_kind and a getter
<prefix>_<Type>_<Variant>_<field> per field, a field without a name
numbered from 0; a getter on another variant is E_ARGUMENT.
pub extern enum is a tagged union by value, as #[repr(C)] lays it out,
its fields C values (NK1145). Part III 15.1, C.3.A C library reports an absent number with a flag, not a status (ADR-284 D30, #88).
T? of a number or of an extern struct at an entry
point comes out as the value’s out-parameter and bool* present; the status
stays OK. The status <PACKAGE>_NONE of D5 and D16 is gone. Part III 15.A task in task::scope may pause at every setting; the scope polls it
(ADR-328 D10, #95).
user_parallelism. A sync task runs on the pool; a pausing one is polled
by the scope’s own future, may borrow, and cannot outlive the frame. The
scope pauses where it is written. NK2102 is retired. ADR-005 D5 changed.
Part II 12.7; Part III C.3.A build-time run that stopped has a code of its own (ADR-321 D16-D19, #525).
comptime runs is NK1244, in the run
time’s words; NK1152 keeps the rule and the bounds. NK1165 is retired:
a folded index past the end stops as the run does (ADR-311 D4 changed). A
steps/ram bound the build does not know, or not above 0, is NK1102,
each with its own sentence. A check with no place to compile build-time code
refuses with NK1127 instead of leaving the value open. Part II 10.2; Part
III C.3.NK1242 and NK1243 had been written into A.2 instead of C.3.A dsl block names its entry, unless the grammar marks one main
(ADR-299 D21, #518).
dsl Json::value { … } eod runs the entry value, as
the call Json::value(text) does. One entry, or one written main entry
rule, needs no name. Several and no main is NK1242; two main entries
are NK1243. ADR-296 D25 noted. Part II 10.2, 10.5; Part III C.3.No debug info unless a project asks for it (ADR-002 D5, #555).
debug, so every build inherited Cargo’s
dev default, full debug info: a hello world was 57 315 976 bytes around
519 674 bytes of code. It now writes debug = 0, and the same program is
764 664 bytes. An abort still names its .nika line.[build.<target>] takes debug (Cargo’s values: 0, 1, 2,
"line-tables-only") for a build a debugger reads; a misspelt key is refused
naming the four there are.debug_info_is_off_unless_the_table_asks_for_it (orchestrator), the
manifest’s codegen test.db everywhere the specification named std::db (ADR-338 D2): Part II
11, Part III 17.2, and ADR-299 D4 and D8 noted.
The database protocol is an official package, db (ADR-338 D2, #539).
Decided, not built: the compiler gives it no place a format lacks. Part III
17.2, 17.3.
json, toml and csv read and write (ADR-338 D5, #539). Decided, not
built. Editing a text in place, comments kept, is left open. Part III 17.3.
std is the core; formats and protocols are official packages (ADR-338,
#539).
json, toml, csv, html and http1 are official
packages, nikaia_json and the rest on crates.io, shipped with the
toolchain, listed in std and taken in by their first use. std::html and
std::http1 move out. Part III 17 and 17.3.A refusal is tested as a file beside the program (ADR-269 D22, #535).
tests/NAME.nika is a test program that may use the
package; NAME.stdout and NAME.stderr say what it must produce, and a
refusal is the compiler’s whole message in NAME.stderr. --bless writes
both. A refused program is only checked; the ones that build are compiled
together. Part III 14.1.A file can be a command, and nikaia new writes a project (ADR-260 D6-D8,
#537).
nikaia run f.nika is the program’s
arguments; a #! first line in the file that holds main is skipped; nikaia
new writes a manifest with name and version, a hello world, its output
test and a .gitignore. Part III 13.1, 13.2.A directory of a project is a package, and a module of one crate (ADR-286 D37-D40, #538).
src/model/rows/ is the package model::rows, reached
with use model::rows and no [dependencies] entry. The packages of a
project are mods of its crate, may use one another in a cycle, and a
directory is a package by what it holds. [directories] marks ignore,
resources, tests or crate. D19 now covers dependencies only.
Part I 9.1, Part III 13.3.A handler’s error where several error types arrive is an Error, one of
them (ADR-280 D30, #512).
Error’s methods, f"{error}" and throw error are
checked; what only one member has is reached through a match. Part I 7.1.inf, -inf and nan are read in any case (ADR-285 D35, #504).
Decided, not built. Part III 17.1.
parse_f32 and parse_f64 read inf, -inf and nan (ADR-285 D35,
#504). Decided, not built. Part III 17.1.
Text becomes a number by a function per type (ADR-285 D35, #504).
text::parse_i32, parse_i64, parse_u8, parse_u32,
parse_u64, parse_f32 and parse_f64, each a T?. Rust’s str::parse is
not offered. Part III 17.1.A list written in number literals is one open number (ADR-285 D34, #513).
[12, 14, 13]’s element type is decided as a let’s
is - by the uses, else i32, else i64 - and NK1116 and NK1200 apply to
it as to a name. Part I 4.5.nikaia fmt indents with tabs (ADR-337 D1, #101).
What nikaia fmt does (ADR-337, #101).
dsl body kept as written, what a file lowers to unchanged,
and --check for CI. Part III 13.2.Every narrowing as is written only where it is shown to fit (ADR-285
D12, D13, #552).
f64 as f32’s rule from 0.0.702 holds for integers
too. A constant that does not fit is NK1116; a value the prover shows to
fit converts with no check; any other narrowing as is NK1241, and the
program writes checked_<T>() (a T?) or truncating_<T>() (the backend’s
conversion). checked_* also converts out of an f64.f64 as f32 is written only where it is shown to fit (ADR-285 D33, #552).
f32 range is NK1116; a value
the compiler proves within it converts; any other as f32 is NK1241, and
the program writes checked_f32() (an f32?) or rounding_f32() (the
backend’s conversion, an infinity beyond the range). Replaces 0.0.701’s
abort. Part I 2.2, Part III C.3.A type the language does not offer is refused; f32 is offered (#470).
usize, isize, i8, i16, u16, i128,
u128 or str is NK1135 outside an extern "C" declaration (ADR-286 D36,
Part I 2.2, Part III C.3).f32 is a type of the language (ADR-285 D33): a float
literal is an f32 where its use asks for one, f32 and f64 do not mix
without as, and f64 as f32 aborts beyond the f32 range (Part I 2.2).What is written down is what can be called (ADR-286 D36, #509).
std’s ledger, a crate’s description nor an extern block
declares is NK1171, also where Rust has one by that name (String::from,
as_str, is_some). std’s ledger stays written by hand: an entry it lacks
is not callable, and the refusal is how the gap is found.NK1171 say so. C.4’s silence stays for a known name
of unknown type (-> ?) and for an unknown type (#497).task::scope (Part II 12.7, ADR-328 D8; #95). task::scope fn(s) { … }
runs its function with a scope. s.spawn fn { … } starts a task that borrows
what the function holds, and the scope returns once every task has ended.
task::Crashed where it ends.user_parallelism = yes the tasks run on the pool (scope_on_pool). At
no they run on the one thread.par_iter() lambda is, at both settings: one that
pauses is NK2102, and one that changes a name the tasks share is NK2107.
A pausing task at no, which Part II 12.7 allows, is not built yet.yes.std entry is handed over rather than lent,
so that the language below can type its parameters.The Node binding’s _async form is a Promise (ADR-284 D9, D19, D28; #88).
For a function that may pause, nikaia bind node adds <name>_async(…).
cancel() cancels the call at its next pause point. The call
then rejects with E_CANCELLED.nikaia bind node (ADR-284 D26, D28; #88). It writes
target/nikaia/c-library/node/<package>.c, an N-API module in C over the
library, and a binding.gyp that builds it against the library with
node-gyp. No napi dependency enters the emitted crate.
Error. Its code is the header’s name for the status, its
status the number, and its message what last_error said.string; bytes are a Buffer (a Uint8Array or an ArrayBuffer
going in); a run is an array.i64 is a number where it is a safe integer, and a bigint beyond.close(), freed by the
collector where nobody closed it.extern struct is a plain object. Its methods are functions of
<Struct>, and a method that changes it writes the fields back.enum is a frozen object of numbers.A library takes the caller’s allocator (ADR-284 D6, D10; #88).
<package>_set_allocator(alloc, free, ctx) hands everything the library holds
to the caller’s functions, alloc(size, align, ctx) and
free(at, size, align, ctx). The library’s heap (c_boundary::Heap) chooses
once, at that call or at its first block, whichever comes first, so every block
goes back to the allocator that handed it out. Called after init or after any
other call, or with a NULL function, it is E_ARGUMENT.
An extern struct’s array field may hold any C value (ADR-284 D15; #88).
Array[T, N] of a scalar, of an enum without payload or of another
extern struct crosses as T name[N] too. Each element is converted as a field
would be, and an element that is no value of its type is E_ARGUMENT.
A library’s _async calls run on threads that are kept (ADR-284 D9;
#88). rt::library hands a call to a thread that is waiting for one, or
starts one where none is idle. A thread that waited 60 s for nothing ends.
Before, every _async call started a thread and ended it. No call waits for a
thread, because a call may pause for as long as its work does.
An extern struct’s Array[T, N] field crosses to C (ADR-284 D15,
ADR-152 D2; #88). A field that is a fixed array of a number or bool is
T name[N] in the header and ctypes.c_double * N (and so on) in the Python
binding, laid out as C lays it out. An array of scalar, of an enum or of
another struct is not exported yet.
A run of pub extern structs crosses to C (ADR-284 D16; #88). An entry
point takes ref Array[T] or Vec[T] of one as const T* and a count. Each
struct is checked as a single one is, and a bad one is E_ARGUMENT. A Vec[T]
comes back into the caller’s buffer counted in structs, with the size query
and E_TOO_SMALL (c-boundary’s hand_back_run). In the Python binding it is
a list in and a list out.
A pub extern struct’s methods are entry points (ADR-284 D16; #88).
<prefix>_<Type>_<method> takes self as const T* where the method reads
it and as T* where it changes it, with no lock, since the struct is the
caller’s memory. A changed struct is written back only where the method
returned. In the Python binding the method belongs to the ctypes.Structure.
A changing method that pauses is not exported yet.
Two corpus measurements re-read for the supervised hello-http (fixed
forward). tests/sharing.rs finds its three counters atomic because they cross
into a supervisor’s children. That is a decided crossing, not the floor’s
answer. The one undecided Shared is still fortunes.nika’s. In
tests/lock_column.rs, hello-http is the one example that opens locks of its
own, each door a single update or get.
develop is green again (fixed forward). Since 0.0.673, two corpus tests
(sequences, typecheck) refused examples/c-library: they check each file
without its manifest, so a library’s entry points were NK1238. Both now drop
that code for a package whose [build] says artifact = "c-library".
hello-http reads request.path() once, and the corpus’s count of method
calls on receivers the loose harness cannot type is back under its ceiling.
SharedMut::supervised(fn { … }) (ADR-327 D3, ADR-328 D9; #95 step 7). A
shared value that is built again when a panic poisons it. The holders keep
their handle, and the door that finds the poison asks std’s restart policy
(supervisor::Backoff):
Both lock shapes, Local and Crossing, carry it. The builder may not pause,
because a door cannot. SharedMut:: needs no use, as SharedMut(…) needs
none. One for all restarts the siblings no longer races at yes: the
crashing child waits for its sibling’s first attempt.
A stream is a generator in the Python binding (ADR-284 D20, D21, D27;
#112). A function taking one callback that answers bool is also
<name>_iter(…), its other parameters the same. The call runs on a thread,
each item waits until the consumer asks for the next, and closing the
generator early answers False, which is the caller’s stop.
A kept function takes what it captures, and a pausing one lends it to every
call (Part I 8.3, ADR-328 D7; #95 step 6). A lambda a callee keeps (a
supervisor’s child, a handler) is called after the statement that made it, so a
value it captures that a move takes away goes with it. Using that value again
is NK2101, with the help a task’s capture has; before, it was rustc’s
borrow of moved value. A lambda that pauses owns such a value once, behind an
Arc, and each call’s future holds a count of it rather than a copy. Before,
the first call moved it out and rustc refused the program. hello-http reads
its address in main again.
The Python binding’s _async form (ADR-284 D19, D27; #112). A pausing
entry point gets <name>_async(…, done=None). With done, the value or the
exception is handed to it on a library thread, and the returned Ticket
cancels the call. Without done, under a running asyncio loop, it returns a
future settled through call_soon_threadsafe, and cancelling the future cancels
the call at its next pause point. What the call holds stays alive until done,
and the ticket is freed after it. Not for a result into a buffer or a call
taking a callback.
nikaia bind python (ADR-284 D26, D27, D29; #112). It builds the library
and writes target/nikaia/c-library/<package>/__init__.py, a ctypes module
over it, from the same reading of the package as the header and carrying the
same ledger hash:
OK is an exception: Error with a subclass per thrown
variant, and BoundaryError subclasses for the seven negative codes, each
carrying what last_error said.str and bytes, and come back with the size query
done by the binding.__init__, its methods, a property
per pub field, close(), a with block, and __del__.extern struct is a ctypes.Structure, an enum an IntEnum, and
null is None.ctypes cannot carry an exception through C.A call refused for its handle (E_REENTRANT, E_ARGUMENT) now says so in
last_error. Not built yet: the _async form’s awaitable, a stream as a
generator, and nikaia bind js, which waits on the WebAssembly build.
A library’s Shared values are safe on the caller’s threads (ADR-284 D1,
D2; #113). A build with artifact = "c-library" keeps the sharing analysis’s
answer at user_parallelism = no too: an entry point’s public signature and a
public field stay at the atomic floor (Arc, the crossing lock), and only what
the analysis proves never crosses gets the plain count. Before this, every
count was plain at no. A handle holding a SharedMut called from two C
threads at once was an Rc raced from both. c-boundary’s handles now require
Send + Sync, so such a value can no longer hide behind the raw pointer.
tests/c_library.rs hits one handle from two pthreads and counts 2001.
hello-http runs under a supervisor (ADR-328, Part II 12.8; #95 step 8).
The server and a refresher are two Permanent children of one OneForOne
supervisor, sharing three SharedMut counters. /crash asks the refresher to
crash. It panics, is started again, and counts on, while the server answers
throughout. /refreshed and /crashes show it, and tests/project.rs checks
it. The address is read inside serve: a pausing child capturing a String
does not compile yet (step 6).
A response body may be bytes, or a file the program never read (ADR-289
D22, D23, D27, D28; #120). The http package’s Response.body is a Body:
Text, Bytes or File. http::File(path, root) looks at the file when it is
made. Its size is the content-length and its type follows its extension (else
bytes). A file that is not there is a 500, and a name that leaves its root a
404, both settled before the status line. The bytes are read through the
reactor only when the answer is sent, and a failure then closes the connection
short and is logged. hello-http serves /source. Fixed on the way: a type a
program declares, or one it names qualified (http::File), is no longer taken
for a std type of the same last name (fs::File) when its cleanup is asked
for. Not built yet: sendfile and the mapping as D24’s other mechanisms, the
send-file key, and D29’s kept mappings.
fs::open and File (Part III 17.1; #546). A file held open is read
(into a list the call changes in place), written, moved about in with seek
(fs::Seek::Start, Current, End; a position before the start is refused)
and measured with len. It is flushed by its cleanup when the scope that owns
it last ends, or with close(). The options are write, append, create
and truncate, and the name is resolved under its root as every fs call’s
is. Each call is the system’s own, as exists’s is. Fixed on the way: a len()
that can fail is not cast to i64. It is one already, and inside a catch the
cast landed on the Result.
std::fs makes, removes, renames, copies and reads directories (Part III
17.1; #546). create_dir and remove (each with recursive), rename, copy,
metadata (len, is_dir, is_file) and read_dir. read_dir gives the names
in byte order, the same on every machine. Each resolves its names under the
root as exists does, so a name that leaves an fs::Root::Dir is Outside
before anything is touched. tests/guide makes its projects with
fs::create_dir and no longer runs mkdir -p. Not built yet: open and
File, and Metadata’s modified, whose type no record names.
.clone() of a text method’s view is text of its own when returned or
assigned (#547). The cause was not the copy but three text methods and
Seq::next, which nothing in std’s ledger described: a function calling one
was taken to pause, and what the method handed back had no type, so .clone()
stayed a view where a String or a String? was wanted. str::trim_start_matches,
str::trim_end_matches, str::strip_prefix and str::strip_suffix now hand back
views into their receiver, and Seq::next the first element or nothing.
trim_matches is left undescribed: below, a text pattern cannot be searched
from both ends. tests/guide drops its .to_string().
text.matches(p).count() is an i64 (#548). str::matches has its entry
in std’s ledger, a Seq of views as split has, so its count() gets the
conversion every count gets. 0 - closes is -1 and the program goes on.
tests/guide drops its as i64. The guide’s SharedMut block is a
fn main() throws, because join throws since 0.0.657.
A pausing entry point is exported _async too (ADR-284 D9, D19; #88).
<prefix>_<fn>_async(…, done, ctx, op) takes the same parameters as the blocking
form and runs the call on a library thread. done(status, ctx) is called
exactly once: with the work’s status, or E_CANCELLED when <prefix>_cancel(op)
came first, which cancels at the next pause point. Cancelling twice or after
done is OK. <prefix>_op_free frees the ticket after done; before it, the
answer is E_ARGUMENT. A NULL op makes no ticket, and a NULL done is
E_ARGUMENT. What C hands the call stays C’s to keep until done; the move to
the library thread goes through c-boundary’s Sent. Not built yet: a library
executor in place of one thread per call.
A library’s entry point may pause (ADR-284 D9, D10; #88). Its blocking
form drives the call on the calling thread over the shared reactor, with the
runtime started first; a task the call spawns runs too. init starts the
runtime, at user_parallelism’s setting. shutdown drains what is still
running within cleanup-deadline and answers E_CLEANUP for what is left;
the process goes on (rt::Started::drain, which finish now uses too). Not
built yet: the _async form with its ticket and <prefix>_cancel (D19).
pub extern struct: C’s layout, crossing by value (ADR-284 D14-D17,
ADR-324 D6; #88). The struct is lowered #[repr(C)] with every field pub.
NK1145 refuses a field that is not a C value (text, an optional, a list, a
struct without the word, …) and its help names the handle. NK1237 refuses an
extern struct that is not pub, and type parameters are NK1145. In a library
build each one is a typedef struct in the header. An entry point takes it and
hands it back by value; an enum field no variant has, or a scalar that is
none, is E_ARGUMENT. Not built yet: Array[T, N] fields at the boundary, its
methods, and a Vec of them.
examples/c-library: a library C calls (ADR-284; #88). A word tally C
keeps as a handle, feeds text and asks; an enum and a callback that stops
the walk. use.c is its C program. tests/c_library.rs builds it in place
under --locked and checks what use.c prints.
null crosses to C as NULL (ADR-284 D5; #88). An entry point takes
ref String? as an address and a length where a NULL address is null, and
an empty text is not. It takes ref T? of a handle as a pointer where NULL is
null, and hands T? of a handle back as a new handle or NULL. Not built yet:
String? handed back, and T? of a number, whose <PACKAGE>_NONE status has
no value yet.
A library’s entry point takes a callback (ADR-284 D5, D11; #88). A
fn(A…) -> R sync parameter is a C function pointer R (*f)(A…, void *ctx) and
void *f_ctx, called on the calling thread; NULL is E_ARGUMENT. The callback
receives numbers, bool, scalar, an enum without payload and ref String
(an address and a length that live for the call), and hands back a number, a
bool or nothing. A call from a callback into a handle the library holds is
E_REENTRANT. Fixed on the way: an entry point that hands back nothing did not
compile. Not built yet: a callback that throws or hands back text, and the
async form’s library thread.
A library starts and stops (ADR-284 D8, D10; #88). <prefix>_init() and
<prefix>_shutdown() are in the header. After shutdown every call is
E_NOT_RUNNING until init. A panic’s poison is cleared by shutdown and
then init; init alone leaves it. The library still starts by itself on the
first call. Not built yet: shutdown draining cleanups within
cleanup-deadline (E_CLEANUP), and set_allocator.
A library hands C a pub struct as a handle (ADR-284 D5, D11, D13; #88).
An opaque <prefix>_<Type> in the header: its extern anonymous constructor
is <prefix>_<Type>_new, its extern methods <prefix>_<Type>_<method> with
self first, a getter <prefix>_<Type>_<field> per pub field, and
<prefix>_<Type>_free. An entry point takes one as ref T and hands a new one
back. Every call holds the handle’s lock - shared for ref self and ref T,
alone for ref mut self - and a call on a handle the thread already holds is
E_REENTRANT; NULL is E_ARGUMENT. Two symbols with one name are refused. The
lock and the raw pointers live in c-boundary. A handle taken by value, a
method taking self by value, and a field whose type does not cross are not
exported yet.
A library’s entry points take and return an enum without payload (ADR-284
D5, D13; #88). It is a C enum in the header, <prefix>_<Type>, its values
<PREFIX>_<TYPE>_<VARIANT> numbered in declaration order; a number no variant
has is E_ARGUMENT. An enum with payload, T?, structs and callbacks are
not exported yet.
A library’s entry point may throw (ADR-284 D7; #88). Each variant of an
error enum the entry file declares and an entry point throws is a positive
status, 1… per library in declaration order, <PREFIX>_E_<VARIANT> in the
header. <prefix>_last_error(out, cap, written) renders this thread’s last
failure with its raise site, into the caller’s buffer; after a panic it says
the library panicked. An entry point throwing std’s errors, or more than one
type, is not exported yet.
A library’s entry points take text, bytes and a run of numbers, and hand text and bytes back into the caller’s buffer (ADR-284 D5, D6; #88).
ref String and Bytes come in as an address and a length; text that is
not UTF-8, or a length with no address, is E_ARGUMENT. ref Array[T] of a
number comes in the same way.String, Bytes and Vec[u8] go out as out, cap, written: out == NULL
asks the size, and a buffer too small is E_TOO_SMALL with the size that
would do, nothing copied.crates/unsafe/c-boundary (ADR-218): every raw pointer the wrapper is
handed is read and written there, each unsafe argued in its README and
checked by Miri in CI, so the generated wrapper writes no *out of its own.A sync function may panic (Part I 7.2). panic stops and never pauses,
as a failed assert does, and the sync inference and NK2202 read it so: a
function that panics may be sync, and an entry point C calls (ADR-284 D5)
may panic on purpose. Found building #88.
A package is built as a library C calls (ADR-284 D4, D5, D7, D8, D12, D13; #88, steps 2 and 3 for D5’s first row, and the header).
[build] artifact = "c-library" makes a shared and a static library and
<package>.h in target/nikaia/c-library/; symbol-prefix names every
symbol, the package’s name by default, and must be a C identifier.pub extern fn of numbers, bool and scalar that neither pauses nor
throws is exported as <prefix>_<name>: it returns an int status and hands
its value back through an out-parameter. A number that is no scalar is
E_ARGUMENT; a panic is caught at the boundary, is E_PANICKED, and poisons
the library for every call after it.nikaia run refuses a library, and a library that exports
nothing is refused rather than built empty. NK1238 is no longer said in a
library build.init/shutdown are refused as not built yet.pub extern fn with a body parses, and four shapes of an entry point are
refused (ADR-284 D4, D5, ADR-324 D6; #88, the first step of ADR-284 §5).
is_extern on a function.NK1237 an entry point that is not pub; NK1238 an entry point in a
build that makes no library - every build for now, since the
c-library artifact is not built; NK1239 a generic entry point or a
trait’s method; NK1240 a parameter or result that does not cross to C,
with the shape that does. The four codes are new in Part III’s table.NK1229: a supervisor’s child that captures a mut binding is refused
(ADR-328, #95 step 5, first half). Its function is called again for every
attempt, and the binding would carry one attempt’s changes into the next; the
help says to build it inside the child’s function or take a .clone() there.
A binding without mut is lent to every attempt. A captured value an attempt
would keep or use up is not refused yet.
supervisor::run restarts the tasks it runs (ADR-328, Part II 12.8, #95
steps 3 and 4).
supervisor::child(fn { … }; restart:) and supervisor::run([…];
strategy:): children start in list order and stop in reverse; OneForOne,
OneForAll and RestForOne; Permanent, Transient and Temporary.
Giving up stops every child and throws supervisor::Escalated. At both
settings, with a pool twin at yes.std’s default policy: the first failure at once, then a delay that doubles
from 100 ms with jitter up to 30 s; an attempt of 60 s starts a new cycle,
and a cycle of 5 minutes gives up. The numbers are starting values.policy: is not built: it takes a trait a program implements, and
std.contracts describes no trait yet.A '…' is a u8 where a byte is wanted (ADR-320, #453 step 2, first
half).
u8 where the use wants one - an
annotation, an argument, a comparison with a byte - written b'a' below, and
a scalar everywhere else. 'é' where a u8 is wanted is NK1103.b[i] on a Bytes is a u8 and b[a..<b] a Bytes in the checker too,
so bytes[0] == 'a' is checked rather than handed to rustc.NK1212, NK1213) and
'\xNN' above \x7F.There is no char: one character is a scalar (ADR-320, #453 step 1,
second half).
char in a type position is NK1135, whose help names scalar and u8.
A ledger that still writes the old word reads as scalar.std.contracts, std::text, the self-hosted tools and the test programs
write scalar; nikaia lower-std regenerated text.rs and package.rs,
and lowering them again changes nothing.net::serve(at) fn(conn) { … } runs each connection in a task of its own
(ADR-326 D1, #121 step 3).
serve at no, its pool twin at yes, as spawn is.
A handler that panics ends its own connection, one that throws says so on
standard error, and neither ends the server. Out of descriptors, it pauses
accepting (D3).std entry whose keeps names a function parameter takes the program’s
kept-function shape (Kept), as a parameter the program declares does.mut parameter is its own value (|mut conn|), not the
address an update block gets; a kept lambda that throws no longer draws
rustc’s unnecessary braces.http::listen still accepts one connection at a time: standing it on
serve needs the bound address and D5’s connections: from serve (#121).join on a task that panicked throws task::Crashed (ADR-328 D8, #95
step 1).
task::Crashed carries message and site (src/main.nika:4), and is
caught as any error is. TaskHandle::join says throws = ["task::Crashed"],
so a function that joins without a catch declares throws (NK2605), as for
any call that can fail.task::scope block is now
refused for its own names rather than for an unknown task::.A panic in a task ends that task, and a poisoned value is never read (#121 steps 1 and 2).
user_parallelism. Its body is dropped, so its cleanups run, and the
program goes on. The hook says a task stopped: there. At yes a task’s
panic used to leave the program spinning, its worker thread gone.Crossing read through the poison,
and Local had none. A release update on Local still counts 5
instructions (callgrind).join of a task that panicked panics in the joiner for now; ADR-328 D8’s
task::Crashed is #95’s first step.A map’s first write types it, and the examples are tested in place.
insert, or first write through the
brackets, gives it its key and value types, as a list’s first push does
(ADR-135 D2). A bare number literal leaves them open, as it leaves a list’s;
compared_numbers stays for that case.tests/examples.rs runs nikaia test --both-settings --locked in
each example rather than in a copy, so a second run lowers nothing. The test
build of nikaia test no longer writes the package’s ledger files: it lowers
the test blocks too, which are no contract of the package’s.examples/json/nikaia.contracts records Json as tethered (#544).Three faults filed by 0.0.652 are fixed.
match error over a sum with a one-variant member draws no
rustc warning: the else copied into that member’s half is marked as the
copy it is.enum declared in another file of
the package carries the enum’s lifetime. The ledger records an enum whose
cases hold a view as tethered, and which types hold a view is read across the
package’s files.assert reads a span with in_seconds() and its kin, which the
ledger already describes as pure; option_defaults.nika drops its let
workarounds. Rust’s as_secs stays out (ADR-150 D5).Layers with phases; the handler runs at most once, under the runtime alone (ADR-335, #121).
.layer(http::Compress()) on a tree: a value whose kind implements
http::Layer with before (typed inputs, may refuse or answer, hands R
to after), after (changes the response) and observe (after the send).next.befores run in written order, at once only where nothing can tell;
afters innermost first, failing open or closed as declared, error responses
included; observe is bounded and cannot change a response.500 are std’s layers. Header
conflicts and inputs a route cannot give are refused while building.Seven compiler faults the move to tests/language turned up are fixed,
each with its tests moved to the Nikaia packages.
test blocks that read a constant the run does not
hold. A test block reading a comptime broke the build-time parser
program, and nikaia test refused a comptime such as 90.seconds() that
nikaia run accepted. A value that reaches such a function is answered by
the interpreter, as before.RustItem<'_>).m["a"] ?? … read past a jump compares with a number. The map’s
element type itself is still not inferred (#543).user-parallelism = yes.x?.f(1) on a field of function type is the field’s call, as
x.f(1) is.375 tests of what a program computes are Nikaia tests now. They were Rust
tests that lowered a program, compiled it with rustc, linked std and ran
it, one program at a time; they are test blocks in tests/language and
tests/build-time, one file per topic, built once per setting by
nikaia test --both-settings and run at both settings, where most ran at one.
What stays in Rust is what a test block cannot say: refusals, the emitted
Rust, the ledger, and programs that need files, input, arguments, an exit
code or a switch.
comptimes and computed defaults are a package of their
own, tests/build-time: a value computed while a package is built is a run
over the whole package (#468), and in one package with the rest they made
its build quadratic.nikaia test keeps each of its builds apart: the test build and the
program at each setting have a directory and a crate name of their own
(target/nikaia/variants/<setting>-<kind>/). In one place each made the
others stale, and every run lowered and compiled all of them again.comptime a test block reads breaks the build-time parser program; nikaia
test refuses a comptime that std’s Rust half computes, which nikaia
run accepts; an error sum whose member lives in another file does not
compile; m["a"] ?? … compared with a number does not compile; a kept lambda
over a SharedMut does not compile at user-parallelism = yes; x?.f(1) on
a field of function type reaches rustc.What the language means is tested in the language. tests/language is one
Nikaia package, a file per topic, whose test blocks run programs and assert
what they computed (ADR-269). nikaia test builds it once per setting and runs
each test in a process of its own; crates/nikaia/tests/language.rs runs it in
place, so an unchanged package compiles nothing.
jumps: its thirteen tests that lowered a program,
compiled it with rustc, linked std and ran it, one program at a time, are
test blocks now, and run at both settings. What jumps.rs keeps is where a
jump may not stand and how it is lowered.The test suite builds std once and keeps it, and runs its compiler
optimised.
std is no longer keyed on the compiler (ADR-002 D4). Its
Rust is committed already lowered, so an edit to the compiler changed none of
it, yet started a fresh tree and compiled std and everything under it
again. The programs lowered there go stale through the switches file
instead, which now names the compiler.target/nikaia-project-tests, set
through .cargo/config.toml’s [env]. Half the tests that ran Cargo wrote
into the developer’s own cache, and manifest, overflow and project
each gave a build an empty one and compiled std from nothing every run.
tools_below keeps its Cargo home and linker at one path, so Cargo no
longer rebuilds it all each run.opt-level = 1 for the dev profile. The tests that lower std and run
the prover in-process (sysroot, sequences, bytes, send) ran four to
seven times as long at 0.a_crates_io_dependency_never_reaches_the_wrapper no longer fails where
CARGO_TARGET_DIR is set, as in the floor job.A tree of routes is a comptime value (ADR-334, #121).
pub comptime ROUTES = http::Routes().get(…)…, mounted under prefixes with
.mount("/api/v1/orders", orders::ROUTES), across files and packages; a
group’s formats replace the server’s below it.State[T], filled
from http::Server(APP; state: [db]), one value per type, checked while
building.comptime may hold a function that captures nothing (Part II 10.2); one
that captures is NK1127.http::Server(ROUTES), and has no .get of its own;
Part III’s examples move to http::Routes(), and the mapped page arrives as
State[fs::Mapped].A function names an array’s length: fn f[comptime N: i64](xs: Array[i64, N]) -> Array[i64, N] (ADR-333 D4, #121).
comptime parameter in a function’s brackets is read off its arguments’
types, as T is; arguments that give it two values are refused; a result
type may name it. Array[T] as a parameter is such a length unnamed.
NK1182’s help offers it.comptime stays the one word for a type’s value parameters too.A grammar run’s parsers share one target directory per machine (issue #178’s build-time parser).
std, winnow, the grammar runtime) is the same
for every parser. Under each project’s workshop it was compiled again for
every new project.build-time-parsers/target).
Each is a crate of its own name (p<key>), so they share the directory
without meeting.dsl_grammar and project no longer compile those
dependencies again.The library build-time code links against is built once per machine, not once per project (ADR-321 D3).
std comes from), its
one file and the toolchain, never on the program. It now lives under the
user cache (build-time-bundle/<key>), as the compiled std does
(ADR-002 D4).comptime or computed default could run. A fresh project
now builds none of them.option_defaults,
attributes, comptime_compiled, proofs_file and field_defaults.A value parameter is marked comptime; a function may take one, written as any other argument (ADR-333, #121).
struct Decimal[comptime precision: i64, comptime scale: i64]: the word tells
a value parameter from a bounded type parameter ([T: Summarize]) where it
is written.fn get[F: Fn](self, comptime path: String, handler: F): the call writes
.get("/orders/{id}"), the argument is held to a comptime initialiser’s
rule (NK1127, NK1152 at the argument), the function is emitted once per
value, and a grammar’s refusal of the path points at the string in the call.
Changes ADR-330 §3. ADR-332’s open question of route’s signature is closed.Routes name their values; a type takes values in its brackets (ADR-332 revised, ADR-333, #121).
{id}, {year}.pdf, {path...} (the rest,
split before decoding, ./../%2F refused, nothing normalised),
?expand, ?page-size={page_size}. The handler gives types and defaults; a
name without a parameter, and a plain parameter the path does not name, are
refused while building. Specificity decides, not order; no optional segments..get, .post, …, .route(methods, path).Header["X-Api-Key"], Cookie["session"], with the
standard names as constants (http::USER_AGENT); a header type per header
is gone. http::Server(formats: …) loses a wrong ;.Decimal[10, 2]; Array[T, N] was the first (ADR-333).The ceiling of unanswered method calls is 17, what the corpus measures, down from 48. The old and the new test both count 17 in 103 programs.
The corpus test of unanswered method calls no longer re-reads the whole
corpus for every tool (sequences::the_corpus_has_no_more_unanswered_method_calls_than_it_had).
For each of the ~80 self-hosted tools it re-read and re-parsed every other
tool, and inferred the whole package’s ledger anew. Each program is now
parsed once, and the tools’ package ledger is inferred once, as lower-std
infers it. It was half of the local test suite’s time.
A handler gets only what its signature declares (ADR-332, #121).
{name} in a route’s path is the parameter of that name; another plain
parameter is the query’s. Every other input is a type implementing
http::Extract, whose extract declares its own inputs as typed parameters:
a header is a type declared once (@http::Header("X-Api-Key") struct ApiKey),
who is asking is AuthUser. Only http’s own extractors read the raw request.Body[T] has no format; the server’s formats and the request’s
Content-Type/Accept choose. A wrapper is read with .value.Fn answers F::params (.name, .ty, .default,
.attribute(X)) and F::result while building; its kind is Fn(F).
NK1232 is retired.route says its path is read while building, a method in the
path, middleware with no result, groups, value parameters of a type.develop is green again: three tests still expected what 0.0.630 and
0.0.638 changed.
tethers: @borrowed and @tethers above a struct are no longer parse
errors. Since ADR-331 an @ line is an attribute. They are refused as
attributes naming nothing (NK1135), and the help does not offer the word
back (ADR-283 D4 stands).samples: the trailing input that must be refused is %%%, since @
starts an attribute.sequences: Seq[char] writes itself back as Seq[scalar].scalar is a type, and char is read as it (#453 step 1’s first half,
ADR-320, Part I 2.6).
scalar, and a '…' literal is a scalar.scalar: This value is scalar, but the let declares
i32.scalar is char, through a type alias in
nikaia_std::prelude.std.contracts’ character methods are keyed scalar:: (is_whitespace,
to_ascii_lowercase and the rest).The rest of step 1 is still to come: moving std, the grammar builtins and
the self-hosted tools to the word, then refusing char with NK1135.
A Bytes is read as a run of bytes (#453 step 5, ADR-320 D6-D7, Part I
2.6).
b[i] is a u8, and b[a..<b] is a run of them. An index outside stops
the program as a list’s does (index::Get for Bytes).b.find(byte; from:) gives the next position as an i64?.b.text() gives the text where the bytes are UTF-8 (a view into the
buffer), and NotText where they are not.std.contracts describes Bytes::text and Bytes::find. A socket read
is made text of and run (#489’s test), and the #472 test compiles its
program.'…' as a u8 is step 2, so a byte is written as a number for now
(b.find(59)).A cut with an open end (#527, Part I 2.6). Inside brackets, [..<b] and
[..b] start at 0, and [a..] runs to the end.
[a..] is parsed as [a..<base.len()], the one form a range has, and is
lowered as the language below’s a.. (index::at now takes a RangeFrom).
The base is read once.find parses. As a body it is refused only for
the line it never declares.s.find(needle; from:) on text (#453 step 4, ADR-320 D6, Part I 2.6).
It gives the byte position of the first match at or after from (default 0)
as an i64?.
from before the
start searches from the start; one past the end finds nothing.nikaia_std::search::find. Before, it fell through to the
language below’s str::find, a usize without from (#452).std.contracts describes it (str::find), so its option is checked as any
other.b.find on Bytes and s.bytes belongs to step 5.Found: a cut with an open end (line[..<sep], line[sep + 1..]) does not
parse, although Part I 2.6 shows both (#527).
A field of a list element is changed in place (#526).
items[1].tags.push(x) read the element through index::get and handed
rustc a shared reference to change: E0596, about a file nobody wrote. A call
that changes a field reached through an index now writes the element as the
place it is (items[1].tags.push(…)), as items[1].push(…) already did. The
ledger reader goes back to the direct form it was moved off.
An attribute’s argument is any build-time value (#496, ADR-331 D1).
@Name(joined(PREFIX, "At")) is computed once where it is written, by the
same path an option’s default takes (contracts::computed_literal). The
ledger records the value it came to.NK1127, NK1152), and the note says it is an attribute’s argument.This finishes #496’s build list.
A type’s and a variant’s attributes are read too (#496, ADR-331 D6, D8).
T::attribute(X) and T::attributes(X) read the walked type’s own
attributes. A function that asks is copied per type, as a walk over
T::fields is.T::variants answers .attribute(X) and .attributes(X).
NK1180’s help lists them.attributes = [...]) and its
variants’ (variant_attributes = ["Add: Tag { … }"]), qualified for a
consumer as a field’s are.A field’s attributes and its default are read while the program is built, and they cross into other packages (#496 steps 4-5, ADR-331 D5, D6, D8).
T::fields, field.attribute(X) is the attribute’s value on
that turn: an X?, null where the field has none.
field.attributes(X) is the list in written order, and field.default is
the default as a T?.NK1235. NK1180’s help lists
the five members.attribute = "field, struct;
repeatable") and each field’s attributes as the values they came to
(field_attributes = ["created_at: Name { value: \"createdAt\", case:
Case::Snake }"]).lib::Name), the way a
dependency’s option defaults are read. A dependency’s field defaults are
qualified the same way.NK1127, said rather than dropped.describe(lib::User { … }))
named its copy describe__lib::User, which rustc refused. It is
describe__lib__User now.Not built yet: T::attribute(X) and a variant’s .attribute(X), and
attributes of types and variants in the ledger.
Attributes are written and checked (#496 steps 2-3, ADR-331 D1-D4, Part
II 10.3). @path(…) lines stand after a declaration’s /// lines and before
a struct, an enum, a variant, a field, a fn (a method, a trait’s method), a
parameter, a trait or an impl.
@meta::Attribute(field, struct; repeatable:
true) marks it. The places are written by their words, struct and fn
among them. Anything else is NK1235, and a second one without
repeatable: true is NK1236.;. A variant of the field’s enum is
written by its name alone (case: snake). A wrong count is NK1101, an
unknown option NK1109, a value that is not the field’s type NK1102, and
an unknown struct NK1135.name: String = 5 is NK1166). A number had passed, having no type of its
own yet.Nothing of an attribute reaches the running program. Reading one
(field.attribute(X)) and the ledger are #496’s steps 4 and 5. Until step 5,
an attribute declared in another package is refused as unmarked.
A struct’s field may have a default (#496 step 1, ADR-331 D5, D8). The
default is written after the type: size: i64 = 50. A literal that leaves the
field out takes the value:
NK1234, said once
beside a misspelled field;NK1166;pub size: i64 = 50)
and reads it back, so a dependency that adds a defaulted field breaks no
consumer’s literal.Part I 4.1’s Page example now lowers and compiles (tests/specification).
An edited nikaia.proofs is a new build (#448): the build cache key
carries a digest of the committed nikaia.proofs and of the files in
nikaia.proofs.d, so removing or changing an entry no longer hands back the
binary built under the old proofs. A project without such a file keeps the key
it had.
develop builds on the toolchain floor again (rust_floor, red since
0.0.620): a match guard with if let is not stable on Rust 1.88. The
dsl block’s arm asks contains_key in its guard and reads the value in its
body.
A dsl html <for> name has the list’s element type (#524).
<for r in :rows>{r.shade}</for> over a Vec[Row], r is a view of a
Row, as a for over a list binds its name. A field Row does not have is
NK1107 where it reached rustc.A tuple pattern’s parts and an empty list’s elements are typed (#522, #523).
(Op::Times, n) => … over an (Op, i64) binds n as the i64, as a
variant’s parts were already bound.let mut rows = Vec() followed by rows.push((fragment, n)) makes rows
a list of what was pushed, for every read after (ADR-135 D2’s first use).
Its elements were unknown.sort_by_key hands back nothing, and its key is the lambda’s result (#497).
[T](mut self: ref Vec[T], f: fn(ref T)) -> ?: the call
was of an unknown type, and the key went unsaid. It is now
[T, K](mut self: ref Vec[T], f: fn(ref T) -> K). A sort used as a value
is NK1103 (()). The examples’ ledgers are unchanged.develop green again (red since 0.0.616): anonymous_constructors
expected take(&S::new) for a parameter that may pause. Since #516 that is
an async closure around the constructor, the only form that fits the
AsyncFn the parameter is.
A par_fold entry of a package’s grammar is driven in pieces (#521).
pieces column (Part III 13.5) marks a grammar entry whose
body is a par_fold. A consumer in another package writes
grammar::parse_pieces with its build’s parallelism, as the grammar’s own
file does. It used to write a sequential parse, which rustc refused.pieces = true on
access-log’s Log::file, and no pub = true on the entries of a grammar
without pub (0.0.618).Two dsl blocks over a grammar in one statement are two values (#518).
Checked::dsl_values), not
under the statement a comptime is keyed by, which two blocks shared.dsl G { … } eod over a grammar the program declares (#518 step 1, #505).
comptime of G::entry("…") would. The value is built into the program,
and the block’s type is the entry’s result. Text the grammar refuses is
NK1178 at the block. It used to be refused by the lowering
(Num isn’t a language the compiler knows).entry: which entry a block over a grammar with
several runs is not decided (#518).Attributes, and defaults on struct fields (ADR-331, #496).
@json::Name("createdAt") before a field: a value of an ordinary struct.
A struct is an attribute when @meta::Attribute(field, …) marks it; the
places are field, struct, enum, variant, fn, parameter, trait,
impl, and in an attribute’s arguments a variant is its name alone.;, so a field added with a default breaks no use.field.attribute(X), T::attribute(X), a variant’s
.attribute(X); repeatable: true allows several, read by .attributes(X).
A field answers .default. Attributes and defaults travel in the ledger.NK1234-NK1236; NK1180 lists the new members.pub grammar offers its entries to other packages (ADR-296 D25, #520).
pub grammar Nums { entry rule number -> i64 = … } parses, where it was
refused as decided and not built. Each entry’s ledger entry is pub as
the grammar is, and a consumer calls lib::Nums::number("42"), which drives
the package’s parser as a grammar of its own file is driven. A grammar without
pub is its package’s (NK1110).par_fold entry across packages, and dsl lib::G { … }, which
waits on #518.A comptime reads an earlier one that holds a std type’s value (#519).
comptime A: time::Duration = 90.seconds() then comptime B: i64 =
A.in_seconds(): the run that computes B gets A as a let of its
constant constructor (ADR-318 D5), which an item may not write. It was
NK1127.NK1127’s note no longer says std’s methods cannot run at build time.
They run in the compiled run (ADR-321). The note now says what happened:
the value could not be compiled to run.A parameter that may pause runs a declared function of either kind, and a list is not collected (#516, #517).
apply(slow, 5) and apply(double, 5) for f: fn(i64) -> i64 both run: a
function that never pauses is handed over inside an async closure, which
is the AsyncFn the parameter is. Both reached rustc before.[1, 2, 3].map(double).collect() is NK1210, with the help to leave out
.collect(): a list’s map and filter hand back a list already.A function named as a value has its declaration’s type (#502, #507 steps 2 and 4).
double, a type’s name for its anonymous constructor (Summary), and a
method (Summary::merge, the receiver first) are fn(…) -> …. A declared
function passed to a parameter that runs it is passed as it is, not as the
&* a kept closure gets, which did not compile.html::escape is -> ref String in the ledger, where it was ?.A tuple position it does not have is NK1107 (#515): t.2 on a pair.
a or an by the word a message names (#511).
check_words::article decides by the sound: an io::IoError, `an `i64,
an f64, `a `u8, a usize``. About forty messages wrote the article by
hand before an interpolated type. The check now corrects every a/an
directly before a backticked word once, at its end. A word that starts with
a sign keeps what was written (an | pattern).A grammar’s bindings have the types Part II 10.8 gives them (#500).
t:term is term’s declared result, and tail:add_tail* is a list of it.
s:until(…), text(…), raw_ident, string, alpha1 and a run of
digit are text, a view of the input. digit, any and char are a
scalar, x? is a T?, and list(item, sep) is a list. An action’s wrong
use of one is refused here, where it reached rustc.nikaia run f.nika names f.nika (#510).
rustc refusal mapped back
to the source, and a panic’s location while it runs, named the copy’s path
(~/.cache/nikaia/run/…/src/main.nika). Both name the file that was run.f[T]() is allowed, with a warning that asks for its use case (ADR-330 D9, #514).
parse[Config](text). Each such call draws NK1233, whose help points to
#514, where use cases are collected. A method’s form is not decided. Changes
ADR-323 §3.Full names stay out (ADR-330 §6, #121).
T::name
is the name the declaration writes.panic(…) has the type never (ADR-276 D27, #506).
let t = if c { "x" } else { panic("no") } is the first arm’s type, and so is
a match with an arm else => panic(…): a wrong use of t below is
NK1103 where it was silent.m[k] ?? panic(…) reads the map as ?? return does, and a panic leaves a
branch for what it took, as a throw does. A function of the program’s own
named panic is an ordinary call.A call through a head nothing declares is NK1181 (ADR-286 D31, D33, #488).
nowhere::wobble(1) used to pass the checker, and rustc answered cannot
find module or crate. It is refused as nowhere::wobble written as a value
is, in a loose file as in a project. Self and the built-in types are heads.fx::read_to_string(…) says
Did you mean fs?, with the use std::fs line where it is missing.A handler’s error has the type the guarded calls throw (Part I 7.1, #501).
fs::read_to_string(…) catch { … error … } binds an io::IoError, and a
program’s own function binds the error type its body throws. A wrong use of
error is refused here (NK1103), where it reached rustc. Several error
types arriving at one handler claim nothing yet (#512).A float literal is an f64 (#503).
f64 is the only float type, so let mut px = 0.0 is one, as is everything
computed from it and a list [1.0, 2.5]’s elements. let n: i64 = px is
NK1103 and px + n is NK1199 here, where they reached rustc.A dsl html block is a String, and a std enum’s case is a value of its
enum (#505 step 1 for html, #507 step 1).
return dsl html { … } eod is text to the checker. A block for any other
grammar still claims nothing.fs::Root::Anywhere is an fs::Root, read from the ledger’s variants
column, as Op::Times is an Op. A case that holds something is its
constructor and is not covered here.A view or a shared handle has the kind of what it reaches; T::access says how (ADR-330 D8, #121).
ref A, Shared[A], SharedMut[A] have A’s kind; T::access is Owned,
Ref, Shared or SharedMut. Replaces the kinds Ref, Shared,
SharedMut of 0.0.610. A? stays the kind Maybe(A).Arrays, tuples, scalar, views and shared handles have a kind (ADR-330 D3, #121).
Array(A, N), Tuple(parts) (walked as T::fields is), Scalar, Ref(A),
Shared(A), SharedMut(A). A view or a shared handle is a kind of its own,
not the kind of what it reaches. NK1232 is left for a function type.A grammar names no type of the program: Named is gone (ADR-330 D7, #121).
Named("Mode") was a type of the program as text, resolved at the block.
Nothing used it; a block maps rows onto a type of the program with -> T.
Changes ADR-323 D1, D3. How a grammar would reach such a type is decided
when one needs to.A type is passed where a function asks for one, and answers its kind (ADR-330, #121).
fn schema[T](T) is called schema(CreateUser): a type described without a
value of it. f[T]() is not decided.::kind, ::fields, ::variants, ::doc and ::name.
::kind is a meta::Type whose parts are types: Maybe(A), List(A),
Map(K, V), Struct(T), Enum(T), Opaque(T). A std type is Opaque
only where std keeps its inside its own.match on a kind checks each arm under the kind it names and emits the arm
the type takes..ty is the field’s type (changes ADR-329 D1); meta::Type’s parts are types
and it exists only at build time (changes ADR-323 D1); a grammar keeps
Record and Named.NK1230-NK1232. Open: arrays, tuples, scalar, ref, Shared;
full names and aliases.benches/envelope builds again (fixes develop, red since 0.0.604).
IoError::NotFound(String::new()), and the variant holds an
fs::Path since 0.0.604 (#449). It builds an empty name now; a PathBuf
is a String’s size, so what the bench counts is unchanged.Part I 7.1’s error type carries an fs::Path (ADR-319, #449 step 10).
ConfigError::NotFound and Unreadable hold fs::Path, and message
shows them with {p.display()}. The example used to be refused with
NK1135 (There’s no type called Path); it now lowers.fs::walk hands back fs::Paths, and to_text() fails only for a name the
system handed over (ADR-319 D5, D7, #449 steps 3 and 6).
fs::walk returns Vec[fs::Path]: a name that is not text is listed as the
platform’s bytes and opens the same file, where it used to end the walk with
NotText.p.to_text() gives the String. On a name the compiler follows back to
text - a literal, text, an f"…" over such names, join or
with_extension - it cannot fail and needs no throws; on anything else,
a parameter or what walk listed, it throws io::IoError (NotText), the
same on every target.tools/sources.nika writes {name.to_text()} where a walked name becomes a
map key. An f"…" that builds a name writes its text parts without
format!.walk example passes the name without ref.file_paths.rs::to_text_throws_by_the_names_source,
a_name_made_from_text_lowers_to_the_call_that_cannot_fail; fs.rs’s
such_a_name_is_not_text and a_walk_lists_a_name_that_is_not_text.A file’s name in an f"…" used as text is NK1201; IoError and
fs::Root::Dir carry fs::Path (ADR-319 D4, D7, #449 steps 5 and 8).
f"…" with an fs::Path in a hole is a name. Where its use asks for
text - a let, an argument, an assignment, a return, a field, a match
or if arm beside text - it is NK1201 at the hole, with
{p.to_text()} and {p.display()} as the ways out. It used to pass the
checker and fail in rustc. A use that asks for a name, and printing,
are unchanged.io::IoError::NotFound, PermissionDenied and Outside carry an
fs::Path, the platform’s bytes; NotText and Other stay messages.
fs::Root::Dir holds an fs::Path; fs::Root::Dir("site") is written as
before. std hands an OS failure’s name over as it was asked, not as its
display.file_paths.rs::a_name_in_a_text_interpolation_is_refused_at_the_hole,
a_name_in_an_interpolation_that_builds_a_name_is_not_refused,
an_io_errors_name_is_a_file_name; error_types.rs and error_sums.rs
write {p.display()}.What fallible collections cost: measured (ADR-327 D4, D5, #490 step 1).
benches/fallible/: 1BRC’s aggregation and benches/taskblock’s tasks,
each written on today’s Vec, String and HashMap and on
allocator-api2’s Vec<T, A> and hashbrown’s map with an allocator that
counts against a budget and may refuse. fallible.sh checks both sides
print the same bytes and counts them with callgrind.allocator-api2’s extend_from_slice copies one element at a time on
stable Rust and doubled the tasks’ count; the bench copies as std does.What a memory block per task costs: measured (ADR-327 D4, D5, #490 step 2).
benches/taskblock/: one workload of many short tasks, each reading a
request and writing a response, in three binaries that differ only in
their global allocator - the system’s, the system’s with a budget counted
per task, and a block of 64 KiB per task. taskblock.sh checks they print
the same bytes and counts them with callgrind.malloc, 17 810 with a budget counted
over it (+2.3%), 10 188 with a block (-41%, half the mispredicted
branches).benches/brc/brc.sh takes the program from the project’s
target/nikaia/bin/, where 0.0.597 (#495) puts it.A static C library a dependency package names links with GNU ld
(#499).
-l as rustc’s own, which rustc writes
before the crates; a function a dependency package calls was not yet
called there, and GNU ld - the default on aarch64 - left it undefined
(rust-lld on x86_64 does not mind the order). -l now goes to the
linker as a linker argument, which rustc writes after every crate.build_time_dependencies.rs::the_build_time_program_links_no_c_library
on arm64, and on x86_64 with -C link-arg=-fuse-ld=bfd.A run reads the defaults the program’s ledger computed (ADR-321 D1, #468, #498).
std’s Rust half computes ("!".repeat(3)) was refused
with NK1127 and the interpreter’s note: the run that computed it lowered
every function of the program, and a call beside it that left an option out
had no value for that option yet. Such a call now stands for
unreachable!, which a run that reached it would be a ring for (NK1168
refuses that first), and a run that stops there is answered as before.comptime’s run reads each default from the program’s ledger instead of
computing it a second time without a workshop.ref String default is computed as the String it views, as a
comptime’s value is.String option with a text default is handed a String, not
the literal’s view, which rustc refused (#498).option_defaults.rs::a_default_std_computes_reaches_a_comptime,
a_text_default_for_an_owned_string_is_one.A call through a head nothing declares is NK1181, as a value is (#488,
ADR-286 D31 and D33). Decided with the owner; not built yet.
nowhere::wobble(1) passed the checker and was refused by rustc; D31
already refused such a head, and the checker asked it of values only.use line (D33) and names a known head one edit away first:
fx::read → did you mean fs?The build-time program leaves a package’s foreign parts out (ADR-321 D14, #479).
comptime of its program
fail with NK1127: the build-time program lowered the whole package, and
rustc found no crate by that name. Each file of the program and of its
dependencies, and each file beside a grammar, is now lowered there without
its extern blocks and without every function and method whose entry may
not run while the program is built - which is every one that reaches C or a
Rust crate, and what D9 refuses a build-time call to anyway.build_time_dependencies.rs::the_build_time_program_holds_no_rust_crate:
a Rust crate whose library leaves a marker when it is loaded (.init_array,
as the ctor crate places it); the build leaves none, the program does.request example takes timeout: time::Duration =
30.seconds() (#374, step 9).Two projects of one name, built at the same time, each run their own program (#495).
<target>/debug/<name> for every project of that name:
between one build and its run another could replace it, and the first ran
the second’s program. There the program is now named for its project as
well, and taken out as the project’s own target/nikaia/bin/<name>, which
run, test and build use. With CARGO_TARGET_DIR set nothing changes.project.rs::two_projects_of_one_name_run_their_own_programs.Part III 15.2’s image example says what the rule above it says (#487,
decided with the owner).
use line says image is a Rust crate declared
with type = "rust".The inline-assembly example names its grammar: dsl x86::Asm (ADR-299
D20, #485).
dsl x86 after use std::backend::x86, which D20
makes NK1228; the grammar is Asm, reached as every grammar from a module
is. ADR-296’s sentence follows.Part III 17.1 lists html::Raw’s constructor as a program writes it (#486,
decided with the owner).
pub fn Raw::new(…) was the ledger’s name; a written Type::new is
NK1149 (Part I 4.2). The listing now declares the anonymous constructor,
impl Raw { pub fn(markup: String) -> Raw }, called html::Raw(markup).A reflected field answers .ty and .doc (ADR-329, #121).
.ty is the field’s type as a meta::Type, a constant of each unrolled turn;
a struct is a Record of its fields, a type already being described or an
enum is Named. .doc is the field’s /// text. Changes ADR-304 D4 and
NK1180.A grammar’s build-time project holds the package’s other files (ADR-321 D13, #479).
main and their comptimes) are now lowered beside it, against a ledger
derived from them.build_time_dependencies.rs::a_grammar_action_calls_another_file_of_its_package.The build-time program holds no C, not even linked: tested (ADR-321 D14, #479).
comptime calls the dependency’s pure
function. After nikaia build the marker does not exist, and ldd on the
build-time program names only the system’s C runtime, the toolchain’s std
and libnikaia_bundle. The program, which calls the C function, links the
library and writes the marker when it runs.build_time_dependencies.rs::the_build_time_program_links_no_c_library
(skipped without pkg-config, cc and ar).Build-time code calls its package’s Nikaia dependencies (ADR-321 D13, #479).
comptime that called a dependency’s function was refused as can pause:
the build-time run derived its ledger against std’s entries alone. Each
dependency’s units are now derived as their package, and the program’s
against std and those entries under the package’s name.pub mod <package> { … }, lowered against its own entries:
lib::tripled(14) in a comptime, and in a grammar action beside a helper
of the program, are computed while the program is built.Parsed knows the package it is a part of (Parsed::package). A run’s
home is the package of the file it stands in: a default of a dependency’s
function is computed in that dependency, where the program’s files are not
visible.NK1152, and nothing
runs.build_time_dependencies.rs.Timers and keep-alive for http (ADR-326 D6-D9, #121).
head_timeout: time::Duration = 10.seconds(): the whole head of a request,
from the accept or the first byte; replaces head_wait’s count of reads.body_bytes_per_second: i64 = 500: the body’s average rate since its first
byte, after a start-up period. Past either, 408 and close.idle_timeout: time::Duration = 75.seconds()
between requests, above common load balancers’ 60 s. Under load the server
ends a connection early only by Connection: close on its next response.net::serve offers a deadline per
read and no idle limit of its own; a WebSocket after its upgrade is not
HTTP’s.An action that reaches C is refused once (ADR-321 D13/D15, #479).
NK2209, and nothing
runs while the program is built. The parser for its grammar then cannot be
compiled, and NK1178 said a bug in the compiler beside the refusal, once
per entry rule. Where the program has another error, a parser that did not
build is that error’s and is not said; with none it still is.grammar_at_build_time.rs::an_action_that_reaches_c_is_refused_once.Part I 6.2’s three places a Shared is written no longer wrap one twice
(#484, decided with the owner).
connect returned a Shared, so keep(Shared(connect(url))) and the field
beside it were NK1123. connect now returns the plain value, and the result
example is fn shared(url) -> Shared[Connection] { return Shared(connect(url)) }.Supervision (ADR-328, #95).
supervisor::run([children]; strategy: …) runs until it gives up, then stops
every child in reverse order and throws supervisor::Escalated. Replaces
Part II 12.8’s start_link.OneForOne, OneForAll,
RestForOne; Kind::Permanent, Transient, Temporary.supervisor::Restart policy decides
(Immediate, Delay, Escalate); std’s grows its delay with jitter and
gives up after a long cycle.mut binding,
or a value an attempt would take, is NK1229.join on a crashed task throws task::Crashed; a scope with a crashed task
cancels the others and throws it.A block under any expression is walked (#494).
if, a match, a try … catch and a select. A block under +,
a cast, ??, a field, an index, a list, a struct literal, a return or an
if’s head was never walked: n * 2 + unsafe { abs(-1) } as i64 left its
function sync and touching nothing, and a comptime calling it ran the C
function while the program was built (ADR-321 D15).tools/foreign.nika’s expression_blocks and contracts/sync.rs’s
visit_expr_blocks now descend through every expression that holds others
(child_exprs); spawn stays out. No ledger in the corpus changes.blocks_in_expressions.rs - the comptime is NK1152, and a pausing
call in an if that is an operand makes its function not sync.An argument made in the call is a source of the keep plan (ADR-283 D24, #493).
tools/buffers.nika) takes an argument a call makes - lent
to a callee that hands back a view and takes no keep - as a source of its
own (Source::Argument). It is kept beside the call, or, where the view
leaves through the function’s result, in the keep the function is given:
return e("y".clone()) makes g take a keep from its caller, and main
declares it. It was cannot return value referencing local variable.(keep).put(…) from the plan, which replaces the
statement keep of 0.0.583.--tethers says argument 1 made in the call to e lives in main’s keep:
handed back by g.views_handed_back.rs.A view handed back is handed back silently (ADR-283 D23-D24, #442).
println(d()) alone in main reached rustc as cannot
find value __keep_91.String the body only reads is a &str - is something to borrow from, so
the result keeps the elision instead of 'static (lifetime may not live
long enough). With two or more such parameters they and the result share
'p, which also keeps a function that hands back a literal compiling..clone(), a struct literal, a call’s
result), lent to a callee that hands back a view, goes into a keep declared
before the statement: e(__keep_args_N.put("y".to_owned())).--tethers says s lives in main’s keep: handed back by d, and what a
borrowed result borrows from (from the caller’s argument for row).views_handed_back.rs, including that following NK1104’s help
runs. stored_views.rs asserted the old 'static for fn first(xs:
Vec[String]) -> ref String, a parameter the body only reads: it now borrows
from the argument, as D24 says, and runs.dsl package::Grammar parses, and a package’s name alone is NK1228
(ADR-299 D20, #485).
dsl takes NAME or NAME::NAME before {; the AST’s Dsl carries the
package (ast.nika). The emitter names the block by its whole path.use line brought in, where a grammar stands, is
NK1228: sqlite is a package, and a dsl block names a grammar, help
dsl sqlite::Grammar. html and a grammar the program declares stay bare.
The help cannot list the package’s grammars yet: pub grammar is decided and
not built (ADR-296 D25).examples/fortunes.nika writes dsl postgres::Sql; so do the refusal and
stored-view tests. Part III’s x86 example (use std::backend::x86, dsl
x86 { … }) is now NK1228 in EXPECTED.txt; its grammar’s name is asked in
#485.dsl_parameters.rs.A map’s read past a jump, assigned to a name that owns its value, is copied out or refused (ADR-293 D15, #492).
x = m[k] ?? return … reads a view of what the map keeps, as the let of
the same does. A value that copies is copied out; one that does not is
NK1102, with m[k]?.clone() as the way out - the rule a T? kept of its
own already followed. It reached rustc as mismatched types.let. Tests:
map_reads.rs.A path in a text f"…" is NK1201; IoError::Other stays a message
(#449, ADR-319 D4 and D7). Decided with the owner; not built yet.
let s: String = f"no file: {p}" is refused at the hole with NK1201, the
code for a hole whose value has no text form; the help names
{p.to_text()} and {p.display()}.NotFound, PermissionDenied and Outside carry the name as an fs::Path;
Other carries a message (D7 had listed it among the name variants).NK1201.A text binding has its length and its class (ADR-314 D1, #491).
digit or hex_digit ({m,n}, +, *) as
text, ref String, as Part II 10.8 says.m..=n, at
least one for +), and binds d of for d in x.chars() within the class:
d as i32 - 48 fits, x.len() * 2 * 10^18 fits for digit{1,2} and not
for digit+. chars and bytes leave a text’s length alone.facts_of_the_walk.rs, typecheck.rs.m[k] ?? return … reaches rustc.ADR-314 D5, measured: the temperature’s checks are proved and cost nothing; the two left are the accumulator’s (#421).
remove-overflow-checks:on, 298.1 with every overflow check off. The
difference is all in sum += temp and count += 1; TENTHS’s line counts
the same in both builds - LLVM had already folded its three checks, so
proving them changes no count. benches/brc/README.md says so and points the
2.1 a row at #432.nikaia lower-std +1.91 %; D5 allows +5 %.A view of what a function owns, or of a parameter, is handed back silently (#442, ADR-283 D23-D24). Decided with the owner; not built yet.
'static applies only where
no parameter is a view as lowered.--tethers shows where each buffer lives.NK2303 say so.Many connections at once, and what a crash leaves behind (ADR-326, ADR-327, #121).
net::serve(at) fn(conn) { … } accepts connections, a task each; http
stands on it, and so can any other protocol.net::Admission strategy
decides beside the hot path; std’s adapts to the load. At the ceiling the
server stops accepting and never ends. Changes ADR-289 D13.user_parallelism; in
main it ends the program (Part III A.2). http answers 500.SharedMut::supervised(fn { … }) value is rebuilt.Vec toward Rust is a
copy.A negation is a site too (ADR-314 D5, #421).
-x of an i32 or i64 that is not a literal
(Checked::negations, by node); the bounds walk proves x above the type’s
least value, and remove-overflow-checks:on writes it <T>::wrapping_neg.
--bounds lists it. Part III 13.3 says so.TENTHS: -value is proved, so every check in the action is.bounds::proven takes the checker’s tables as one bounds::Sites.A grammar’s action is proved from what its bindings matched (ADR-314 D1, #421).
x:dec[T](…) is a T, c:digit and c:hex_digit a char, where the
grammar has no rule of that name. Before, every binding was unknown, so no
operation in an action was a site at all, and a digit put into an i32
field went unsaid (typecheck.rs’s grammar test did that; it now writes
dec[i32](digit+)).dec[T](digit{m,n}) binding starts in 0..=10^n - 1, a digit binding’s
code in 48..=57, unless the action binds the name again. --bounds
lists actions’ sites.TENTHS: whole * 10 and whole * 10 + text::digit_value(frac) are
proved; n * 10 over dec[i64](digit+) stays checked.char and an
ensures.tests/facts_of_the_walk.rs (was char_codes.rs), the report and a
run with the proved checks dropped.A callee’s ensures is a fact at the call; an expression function
publishes what it returns (ADR-314 D3, #421).
sync function whose body is one return e, with e a whole number the
prover reads, gets ensures = ["result == e"], from = ["return"] in the
ledger. text::digit_value ensures result == c - 48 (std.contracts);
Part III 13.5 says so.prove_terms), which is what lets c as i32 - 48 be read.std’s, by a bare name or a path - as a
number of its own of which each ensures holds, a char argument as its
code. text::digit_value(c) * 10 is proved to fit an i32.nikaia’s recursion_limit is 256: the parser’s combinators are now
instantiated along a longer path.A char’s code is a number of the proof (ADR-314 D2, #421).
c as T of a char into a whole number
(Checked::char_codes); the bounds walk reads it as a number of its own in
0..=0x10FFFF, handed to every proof like a length’s bounds.c as i32 - 48 and, through a let, code * 2 + 1 are proved to fit;
c as i32 * 4096 stays checked.tests/char_codes.rs, the report and a run with the proved checks
dropped.A dependency’s prover questions are its own (ADR-270 D23, #448).
nikaia.proofs open (proofs::for_unit): its answers are read from
there, and nothing it asks is written into the program’s file. The package
writes its own when it is built.nikaia.proofs.--locked says where a question it would have searched was asked (ADR-270
D22, #448).
src/main.nika:3, in f``: the file,
and the function and its line for the bounds walk’s questions. The prover’s
passes ask for a whole file at once, so theirs name the file.The tools below Nikaia take no orders from the project (ADR-325, #480).
rustc and what rustup starts run in a directory of the compiler’s
own under the cache (where_tools_start), never in the project: a project’s
.cargo/config.toml (a linker, a runner) and rust-toolchain.toml are not
read. Every path handed to them is absolute.CARGO_HOME, environment and rustup default still apply.CARGO_HOME linker does run.A dsl block names a grammar from a package as package::Grammar, always
(ADR-299 D20, #485).
dsl sqlite::Sql(schema: APP) { … } eod, as every name from a package is
written (Part I 9.2). No short form, not even where package and grammar share
a name; a grammar the program declares stays bare (dsl Json).dsl sqlite { … } is NK1228, naming the package’s pub grammars.dsl sqlite::Sql.:: after dsl.A package links the system libraries it names, through pkg-config
(Part III 13.4, ADR-324 D2-D4, #483 steps 2-6).
[library.<name>] with a required pkg-config is read from
nikaia.toml. Any other key there is refused.NK1226 both ways: a block’s extern(library: "x") without an entry,
and an entry no block names. It is checked per package before anything is
linked.pkg-config --libs for each entry.
NK1224 where pkg-config is missing or does not find the library. It
names the package, the library and the name looked for.-l, -L, -pthread (and on macOS -framework, -F) are
taken. Any other flag is NK1225, named, so a .pc file cannot load a
linker plugin.rustc for the program crate only, through
the wrapper, and never for the build-time library (ADR-321 D14). A program
that uses such a package writes nothing for it.examples/sqlite is a project with [library.sqlite3]. Its
end-to-end test builds it with nikaia run and passes no -l.Every open issue carries one status label (docs/README.md, CLAUDE.md).
ready (13), decision (26), blocked (16) and note (12), set on all 67
open issues from a reading of each issue and its comments against the code.decision: its To-build list is built, and whether two remaining
limits belong to it is the owner’s call.The commit that finishes an issue says Fixes #N (CLAUDE.md).
promote closes only what a commit names with Fixes, Closes or
Resolves. Six finished issues stayed open because their commits wrote
(#N), or because they were built under another issue’s To build list.The tools below Nikaia read no configuration from the project (ADR-325, #480).
rustc and every tool rustup starts are started in a directory of the
compiler’s own, outside the project’s tree, so a project’s
.cargo/config.toml or rust-toolchain.toml is never read. A pure Nikaia
project could run a program of its own while it was built through one.nikaia.toml; the user’s
CARGO_HOME, environment and rustup default still apply. The cache stays
where it is.extern without a convention, and extern(library: "x") (ADR-324 D6,
#483 steps 1, 4 and 5 in part).
extern { … } and extern(library: "sqlite3") { … }.
The AST carries the library.extern "C", or any other string there, still parses and is NK1227 from
the checker, with extern as the help.unsafe extern "C" as before.examples/sqlite and the tests write extern.extern blocks of Part III 15.1 and 13.4’s
extern(library: "sqlite3") compile again.
tests/specification/EXPECTED.txt and COMPILES.txt are regenerated.[library.<name>] entry and NK1226 (step 2), and linking
through pkg-config (step 3). Until then examples/sqlite still needs
-l sqlite3.Every bench and example lowers byte for byte as before.
extern(library: "sqlite3") and [library.sqlite3] pkg-config = "sqlite3";
extern without "C" (ADR-324 D2, D3, D6, #483).
extern block names its system library; the manifest’s
[library.<name>] entry, with pkg-config required, says how it is found.
A name without an entry, or an entry no block names, is NK1226.pkg-config; without it, or not found:
NK1224. The -l fallback is gone.extern is the C convention, the only one: extern { … }, pub extern fn,
pub extern struct. extern "C" is NK1227. Changes ADR-302 D1’s and
ADR-284 D4’s spelling; every block of the specification follows.extern blocks are refused by today’s parser,
and the recorded verdicts say so.A body’s last expression is a typed slot like return (ADR-285 D32,
#439).
fn count(xs: ref Vec[i64]) -> u64 { xs.len() } now asks the bounds walk,
as return xs.len() already did, and is taken.fn f(a: u32) -> i64 { a << 3 } is now NK1215 there as well. Before,
it was widened after the narrow shift without a word.Every bench and example lowers byte for byte as before.
x += a over two integer types is x = x + a (ADR-285 D32, #439).
x += a when they were two
different integer types, so total += a with total: i64 and a: u32
was refused by rustc.total += i64::from(a). The value’s own
operands are widened as a stated type’s are, and a bit operation inside it
is NK1215.x’s (small += b with
small: u32, b: i64), the result does not fit: NK1105, with the
conversion as help.u64 with i64 has no common type and stays NK1199, as for +.Every bench and example lowers byte for byte as before.
A map’s trust is the map’s (ADR-010 D1, #431).
contracts::trust::trusted_maps):
let makes it (let m = collections::HashMap());clone, an index, or a for;w are told apart.std names as a source anywhere, a lambda,
a match or a catch in the function.trust.rs no longer argues from “one input buffer”.Bytes has no way to text in the
ledger.Every bench and example lowers byte for byte as before.
docs/open-decisions.md is retired; a question for the owner is an issue
labelled decision (6a7f4ead).
Shared twice in Part I 6.2), #485
(how dsl names a package’s grammar), #486 (html::Raw::new in Part III
17.1), #487 (the image comment in Part III 15.2) and #488 (a call through
a module nothing declares).docs/README.md, the roadmap, the ADR template and ADR-258 D5 point at the
label; an ADR that answered one of its questions names the former page in
plain text.There is no build script; a package links a C library by name (ADR-324, #483).
build.nika and std::build are withdrawn: no program of a
package runs before or during a build.extern "C" blocks need; the
compiler finds them through pkg-config or by name and links them into every
program that uses the package (NK1224 when one is missing).-l, -L, -pthread, -framework and -F pass from pkg-config
(NK1225), after Go’s CVE-2018-6574.#include
names, past ADR-310.A warning is said once (#481).
nikaia run file.nika printed every warning twice.
?? and ?. after a value that cannot be absent warn (ADR-279 D13,
#476).
x ?? 0 over an i64, and find(x) ?? fallback over a callee that now
returns User, build with the warning NK1216. The answer is the left
side. The fallback is checked, is not written below, and takes nothing.s?.len() and u?.name on a plain value build with the warning
NK1217. They are the plain call or read, with that member’s type.xs[2] ?? return false is still
NK1211, and users[2]?.name is still NK1121. The help asks for the
length check, because the line promises a guard it does not give.?. over a T? is now a_safe_call and
a_safe_field.Every bench and example lowers byte for byte as before.
A grammar calls its package’s Nikaia; no foreign code runs while a program is built, not even by being loaded (ADR-321 D13-D15, #479).
std only. A
package with extern "C" or Rust crates is compiled again without them, since
loading a library runs its constructors before any call.The prover’s logic layer is a package any package may ask, also while it is built (ADR-270 D24-D26, #478).
verify. Not public: the solvers.nikaia.proofs. It gets
the reference solver’s answer under a counted budget, the same on every
machine; unknown proves nothing.std::logic.WHERE rules out a NULL
(ADR-299 D18) without writing a solver of its own.use.A dsl block may name the struct its rows are: dsl … -> T { … } eod
(ADR-299 D15-D19, #114; build: #477).
NK1219, a field without a
column NK1220. extra_columns: true and missing_fields: true (only for a
T? field) relax it; both are the compiler’s and never reach the grammar
(NK1223).NULL goes only into a T? field (NK1221). A NULL
that reaches another field while the program runs fails execute with an
error naming the field, the column and its table.NK1222; a field type that is
neither a scalar, its T? nor a group is NK1218, Shared[T] among them
(#475, after the MVP).A value the compiler shows is not negative goes into an unsigned type (ADR-285 D32, #439 steps 9 and 10).
let n: u64 = xs.len();let n: u64 = k after if k >= 0;let n: u64 = k after assert(k >= 0);i64/i32 values into u64, and i32 into u32, at the other
typed slots.proofs::ask, so the answer is
recorded in nikaia.proofs) whether the value is >= 0 where it stands.
A proved value is written nikaia_std::num::u64_of(k), without a check.k as u64 (checked) and assert k >= 0.assert claims as a fact after it.0 <= len(x) for a list whose elements take no space, which
has no upper bound.Every bench and example lowers byte for byte as before, and their
nikaia.proofs do not move.
?? and ?. after a value that cannot be absent warn; after a list’s index
they are still refused (ADR-279 D13, #476).
x ?? y where x is not a T?: the warning NK1216; the answer is x, and
y never runs.a?.m where a is not a T?: the warning NK1217; it is a.m, with the
member’s type.T?, NK1211 (??) and
NK1121 (?.) still refuse the line, with the length check as the help.-> User? to -> User, a driver
that sees WHERE email IS NOT NULL) no longer breaks the callers.The help says when a number goes into another type on its own (ADR-285 D32, #439 step 11).
NK1102–NK1106 between two number types:
NK1199’s note names the reason:
f64, the conversion can change the value.tests/errors/EXPECTED.txt does not move: no program in the corpus meets
these lines.NK1215: a computation whose result depends on the width says which width
(ADR-285 D32, #439 step 8; #474).
let t: i64 = a << 3 with a: u32 is now refused, and so are !a,
a & b and a.wrapping_mul(3) in the same place. The help names both
readings: (a as i64) << 3 computes in i64, and (a << 3) as i64
computes first and widens the result.
Before, these were widened after the narrow computation, without a word.(a as i64) << 3 was written a as i64 << 3 below, which rustc
reads as generic arguments. A conversion left of << now keeps its
parentheses, as one left of < does.Every bench and example lowers byte for byte as before.
A stated type reaches the operands (ADR-285 D32, #439 steps 6 and 7).
x * y over two i32s, put where an i64 is stated, is now computed in
i64: i64::from(x) * i64::from(y). So area(50000, 50000) prints
2500000000 instead of stopping at the overflow. The stated type reaches
through +, -, *, /, % and a negation. A literal among the operands
is of the stated type. A name, a call or a bit operator is widened as it is.let t: i64 = a * a over an i32 constant is folded in i64, and no
longer NK1116.let p = x * y is still computed in i32, as written.let n = 5 used as an i64 and as a u32 stays
NK1200. Widening takes no part in inference.Every bench and example lowers byte for byte as before.
Every pair of integer types compares as numbers, and a type variable is not widened (ADR-285 D32, #439 steps 4 and 5).
a < b with a: u32 and b: i64 widens the narrower side, as an operator
does.u64 against i32 or i64 has no common type. It is written as
nikaia_std::num::unsigned_with_signed (or signed_with_unsigned), which
tests the sign and then compares, so (-1 as i64) < (0 as u64) is true.
Before, the checker passed these and rustc refused them.pick(a, b) for fn pick[T](a: T, b: T) with an i32 and an i64 is now
NK1102, with the help a as i64. Widening takes no part in inference.
Before, rustc refused it.Every bench and example lowers byte for byte as before.
Integers widen where no value can be lost (ADR-285 D32, #439 steps 1–3).
let x: i64 = a with a: u32 now compiles, and so does the same at an
argument, a return, a body’s last expression, a field in a struct literal,
an assignment and an element of a list literal whose element type is stated.
The value is written i64::from(a) below.u32 + i64 is computed in i64, and so are the other operators except the
shifts. [a, b] over a u32 and an i64 is a Vec[i64], whichever comes
first.common_integer holds Part I 2.2’s table. u64 with a signed type has no
common type: it stays NK1199 in an operator, NK1154 in a list and
NK1102–NK1106 in a slot. Narrowing stays refused, and a list that already
exists is not converted.v.push(a) on a Vec[i64] widens like any argument. v.push(b) with
b: u64 used to pass the checker and was then refused by rustc. It is now
NK1102.tests/specification/EXPECTED.txt: Part I 2.2’s fn area block is no longer
NK1104. Its x * y is widened after the multiplication. Computing it in
i64 is step 7.Every bench and example lowers byte for byte as before.
A file’s name is printed as its bytes, and what is printed is only read (ADR-319 D4, #449 step 5; ADR-279 D5, #473).
println(f"found: {p}"), print(p) and the stderr halves write a name’s
bytes as they are (nikaia_std::fs::path::print), alone or joined into an
f"…". The name reaches the output unchanged.print(s) then println(s) was NK2105, because a print call
takes any value by ?, which counted as kept. An argument of the four print
calls is now lent, as ADR-279 D5 says and as the emitted print! always
did.Every bench and example lowers byte for byte as before.
An f"…" with a file’s name in it is a name, joined by its bytes
(ADR-319 D4, #449 step 5, first half).
let bak: fs::Path = f"{p}.{n}.bak" and fs::exists(f"{p}.bak", root)
now compile.nikaia_std::fs::path::joined):
the name by its bytes, text and numbers by their text. A name that is not
text keeps its bytes.f"…" are names. Where none is, the
literal is format! as before.f"…" used as text (its code is asked
in #449), printing a name’s bytes as they are, and a let without a type
taking Path from its first use.Every bench and example lowers byte for byte as before.
What a program does with a file’s name, it does on the fs::Path
(ADR-319 D3, D6; #449 steps 4 and 7).
starts_with, ends_with, file_name, extension, with_extension,
parent, join and display() are methods of fs::Path.== and != compare a name with a name or with text.std’s own function (nikaia_std::fs::path), because Rust’s Path
has methods of these names that compare whole components: there,
"data.csv".ends_with(".csv") is false and dir//data.csv equals
dir/data.csv.
caf\xe9.txt with the extension bak is caf\xe9.bak.display() shows � where a name is not text.fs::Path and which
comparisons have a name on one side, and the emitter writes those as
calls into nikaia_std::fs::path.f"…" by use (step 5), to_text() by source (step 6, which
waits for #431), and walk returning names (step 3’s second half).Every bench and example lowers byte for byte as before.
One integer type takes another without as where no value can be lost
(#439, ADR-285 D3 amended, D32 new). Decided with the owner; not built yet.
u64 with a signed type has none and stays NK1199 / NK1154.NK1200 and a generic T over an
i32 and an i64 stay refused.+ - * / % and a negation to the operands;
a bit operator or a wrapping_/saturating_/truncating_ name computed
in a narrower type there is the new NK1215.u64 without as.A grammar describes a type as data, and a row’s fields are named by table
(ADR-323; ADR-299 D12-D14; #114). meta::column(name, type) and
meta::parameter(name, type) take a std::meta::Type — scalars, Maybe,
List, Record and Named — from which the compiler writes a dsl block’s
row and parameter types; a message about such a type names the block that
declared it. A row has a field per table (u.a.id) and a flat field per
unique column name; a shared name is no flat field, and NK1107’s help names
the qualified paths and AS; a name that cannot be a field is refused at the
query. Whether two blocks with the same columns share a type is not decided.
Decided and specified; not built.
A socket read through a method is an untrusted source (#472, ADR-010 D1/D2).
--trust looked up each call by its name, so c.read() on a
net::Connection was never seen.net::Connection::read makes the program untrusted, and its maps
get the keyed hash.k-nucleotide calls wanted.as_str(), which
std.contracts does not describe, so its map is now a HashMap. 1BRC and
access-log stay trusted.net::Connection::read is the only untrusted entry a program reaches as a
method, and it is tested.std::fs asks for a file’s name (ADR-319 D2, #449 step 3, first half).
fs::create, map, read, read_to_string, walk, exists and write
take path: ref fs::Path instead of path: ?.fs::read_to_string(std::path::Path::new(&(name)), …). An fs::Path is
lent.fs::write only reads the file’s name, so fs::write(self.path, …) no
longer reports self.path as handed over (NK2106); the data it keeps
still is.walk still returns text. Returning Vec[fs::Path] waits for the path
operations (step 4) and f"…" by use (step 5): std’s own sources.nika
calls ends_with on a walked name and puts it into an f"…".A number literal where no number is declared is refused (#471).
let s: String = 3, a return -2 from a function declared -> String and
P { name: 1.5 } passed the checker and failed in rustc.NK1103, NK1104 or
NK1106, naming i64 or f64.i64?), takes the literal as
before.Every bench and example lowers byte for byte as before, and the specification’s blocks keep their verdicts.
fs::Path exists, and text stands wherever a file’s name is asked (ADR-319
D1–D2, #449 steps 1–2).
std’s Rust half declares fs::Path as the platform’s own bytes (a
PathBuf), and std.contracts describes it (pub, crosses,
compares).ref fs::Path lowers to &std::path::Path, the way ref String lowers to
&str.let with a declared type, an
argument, a field, a variant’s part, a return or an assignment.
std::path::PathBuf::from("a.txt").std::path::Path::new(&(name)).Wrap::Path, Wrap::PathView).NotFound(Path) written as NotFound(fs::Path) now compiles.std::fs signatures (step 3), the operations (step 4),
f"…" by use, to_text() and display().Every bench and example lowers byte for byte as before.
develop is green again after 0.0.534. nikaia_std::build_time::Parts
stood after the module’s tests, which clippy refuses
(items_after_test_module); it now stands before them.
A span of time can be computed while the program is built: comptime T:
time::Duration = 30.seconds(), and an option’s default of the type
(ADR-318 D3–D5 with ADR-321, #374).
constant column (Part III 13.5).
std.contracts gives time::Duration constant =
"time::Duration::new(secs: u64, nanos: u32)", and adds the constructor’s own
entry, which is not pub.std’s Rust half
(nikaia_std::build_time::Parts) and hands it back as the constructor and
its parts. The compiler holds it as a new build-time value,
BuildValue::Constant.comptime of the type lowers to const T: time::Duration =
time::Duration::new(30, 0);.timeout: time::Duration =
time::Duration::new(30, 0). A call that leaves the option out gets that
value, in the same package or in another one.30.seconds() as a default was NK1127 until now. A list of text is still
refused as a default, and the help still offers a T?.Every bench and example lowers byte for byte as before. std.contracts
changes only in time::Duration’s entries.
The specification’s blocks and the module tests are checked through a workshop, and a lowering asks for build-time values once (ADR-321 D1, #468 stage 4).
specbook::verdicts and report take a workshop. --example specification
uses the user’s build-time workshop, as a build of a loose file does, and
the tests use their shared one. EXPECTED.txt is unchanged.tests/modules.rs reads and lowers its programs with a workshop.comptime twice. Before
lowering, it asks each file what it uses (Needs::of), and that check
computed the values again through the interpreter. Build::leaves_values
now leaves them alone there.With this, no test outside a run’s own sub-program reaches the interpreter for a call. Inside one, a computed default is still checked again. Every bench and example lowers byte for byte as before.
The comptime and default tests run as a build does, through a workshop
(ADR-321 D1, #468 stage 4). tests/comptime.rs, tests/comptime_item.rs and
tests/option_defaults.rs now check, infer and lower with a workshop, so they
test the compiled path rather than the interpreter. The helpers live in
tests/common.
emit_program_reading compiles an option’s default in the workshop its
Reads names, as a project build does."hallo".to_uppercase().
It is now computed, as "HALLO". The refusal they also covered, a callee
nothing declares, still gets NK1127, naming the callee.develop is green again after 0.0.529. tests/expressions.rs still
counted 1.5E-4 among the exponents a float may carry; ADR-322 refuses it, and
tests/number_literal.rs holds the refusal. The test now lists the lower-case
spellings only.
A comptime raises its own bounds: comptime(steps: 50G, ram: 16Gi)
(ADR-321 D11, #468 step 5b). Built as decided in 0.0.527.
comptime(steps: 100) F = fib(20) is refused by the budget, F took more
than 100 steps while the program was built.comptime(ram: 64Mi) stops a list that grows without end at 67108864 bytes.comptime(stpes: 5) is NK1109, A comptime has no bound called
stpes., with Did you mean steps?.NK1102, and nothing
runs.The bounds are part of the run’s program, so a changed bound runs again. Every bench and example lowers byte for byte as before.
1G and 4Gi are numbers, and an exponent is written e (ADR-322,
#469). Built as decided in 0.0.527.
K M G T P or Ki Mi Gi Ti Pi, after its
digits and separators: 1_500M is 1500000000, 4Gi is 4294967296. The
value is lowered, never the suffix, and the use gives the type (1G is an
i32, 3G an i64, 1K in a u8 is NK1116 about 1000).u64 the number is refused by name: 20000P is
20000000000000000000, which is too big.1Gx is still a number beside a name (NK1117); 0xFFK is still a digit
the radix does not have.1.5G is refused: A scale like G stands on a whole number, not on a
float. 1E5 is refused with 1e5, and 1.5E-4 with e-4.No .nika file in the repository used either spelling; every bench and example
lowers byte for byte as before.
The ledger’s pass no longer computes a comptime (ADR-321 D1, #468). The
ledger’s inference checks every file to learn what each call resolves to. That
check also computed every comptime and computed default, through the
interpreter, before the real check compiled and ran them again.
Shape::Num(1.5)) is a value written
down, not a call: it no longer sends the initialiser to be compiled.Every bench and example lowers byte for byte as before.
A whole number may carry a scale, and a comptime names its bounds
(ADR-322; ADR-321 D11). 1G is a billion and 4Gi four gibibytes: a decimal
whole number may end in K M G T P (decimal) or Ki Mi Gi Ti Pi (binary),
which is a spelling of its value, typed by its use like any literal. An
exponent is written with a lower-case e; 1E5 is refused. A comptime
raises its bounds at its declaration, comptime(steps: 50G, ram: 16Gi) TABLE =
…, and NK1109 refuses another name. Exa and a scale on a float are not
decided. Decided and specified; not built.
An option’s computed default is compiled too (ADR-318 D1, ADR-321 D1,
#468). A default is a comptime without a name. It is now computed as one is:
compiled and run in the build’s workshop, both where the checker asks about
it and where the ledger records its value.
times: i64 = "abcd".len() was NK1127, because its body is Rust; it is now
4, and a call that leaves the option out gets 4.base: i32 = sq(100000) stops the build at the multiplication, pointed at
the default.A read with no workshop still gets the interpreter. Every bench and example lowers byte for byte as before.
A build-time call the rule forbids is named as it resolves (ADR-321 D2,
#468). The rule was read off the ledger before a comptime was compiled,
but the refusal named the callee only when it was called by a bare name. It
now also names one called by its path (io::read_to_string) and a method by
its key (Reader::read), looked up in this program’s entries and then in
std’s.
build_time’s 47 tests now run through a workshop, as a build does, so they
exercise the compiled path. Three of them now expect ADR-321’s answers:
io::read_to_string() is not allowed (NK1152), where it was NK1127;Array does not have stops the build in the program’s words,
where it was NK1165.A program that declares a grammar compiles its comptimes too (ADR-321
D3, #468). The library build-time code links against now names the grammar
runtime beside std, so a program with a grammar is lowered whole into a
comptime’s program, and an ordinary call in it runs compiled. Running the
grammar itself, Cfg::file(…), stays the grammar run’s (ADR-177); whether it
joins this crate is ADR-321 §6’s open question. Every bench and example lowers
byte for byte as before.
What an earlier comptime came to is carried into a later compiled one,
whatever its shape (ADR-321 D1, #468). A comptime program carries the
earlier ones as the literals they came to. Lists, tuples, structs and variants
are now carried as well as numbers, truth values and text. So
comptime BIG = ORIGIN.scaled(10).sum() after comptime ORIGIN = origin(1, 2)
is compiled rather than handed to the interpreter. Every bench and example
lowers byte for byte as before.
A file the build reads is handed to a compiled call (ADR-310, ADR-321 D1,
#468). comptime N = twice(asset("data.txt")) went to the interpreter. Now the
build reads the file by ADR-310’s rules, the asset stands as the text it
read, and the call is compiled and run. A file the allowlist does not name is
NK1175, and a computed path NK1176, before anything is compiled. A program
with a grammar is still the interpreter’s. Every bench and example lowers byte
for byte as before.
A comptime in a program of several files is compiled and run too (ADR-321
D1, Part I 9.1, #468). Each file of the package is lowered with the others into
the comptime’s program, as a package is lowered, so a callee in another file
runs from its own body. A stop there is named by the function it is in:
It stopped in sq, in another file of the program. A program with a grammar
and an initialiser that reads an asset are still the interpreter’s. Every
bench and example lowers byte for byte as before.
A comptime may call a method, std’s or the program’s (ADR-321 D2,
#468). comptime LOUD = "abc".to_uppercase() was NK1127, because the
interpreter could run only code written in the program; compiled, it is
"ABC", and Point { x: 3, y: 4 }.sum() runs the program’s own method.
Any initialiser with a call in it is now compiled. The rule is asked of what
it reaches: the ledger derived for the function that hands the value back, so
a method that prints or pauses is NK1152 before anything is compiled, naming
the callee where the initialiser calls it by name. An initialiser that reads an
asset, a program of more than one file and one with a grammar are still the
interpreter’s. Every bench and example lowers byte for byte as before.
A build-time run is bounded: steps, call depth and memory (ADR-321 D7, D10, D12; #468, second stage).
comptime’s program counts every call and every turn of a loop
(nikaia_std::build_time). Past ten billion steps the build is refused with
NK1152, F took more than … steps, naming the path it was in. A loop
that never ends and fib(50) are refused rather than hanging the build.NK1152 too, named as that.NK1152.comptime it
is, every ten seconds after.The allocator is in the library and not in each run’s program, because a
program linked against std dynamically uses its library’s allocator. How a
comptime raises a bound (D11) is not decided; this compiler’s tests ask a
workshop for smaller ones. Every bench and example lowers byte for byte as
before.
A comptime that calls a function is compiled and run (ADR-321 D1, #468,
first stage). The initialiser and what it calls are lowered as the program is
and compiled for the machine that builds, so the build-time answer is the
run-time one:
comptime C = sq(100000) with an i32 sq stops the build at the
multiplication, NK1152 with the program’s words (attempt to multiply with
overflow), pointed at the line it stopped on. The interpreter computed
10000000000 in a wider type.std’s Rust half: text::digit_value('7') inside a
callee was NK1127, and is 7.NK1152 as before.How:
std (D3, the
bundle), built once in the build’s workshop beside its cache.rustc directly, and its answer is kept under a key of
the code, the toolchain and the bundle (D4). An unchanged comptime is
neither compiled nor run again, which also keeps the lowering inside
Cargo’s build from starting another.The first stage takes a program of one file without a grammar, and an initialiser of numbers, names and calls to the file’s own functions. Elsewhere, and in a bare check with no workshop, the interpreter still answers. The step budget, one crate per package and the interpreter’s removal are the next stages. Every bench and example lowers byte for byte as before.
A build-time run is bounded in steps and in memory (ADR-321 D7, D10-D12).
The step budget is 10 000 000 000 by default, chosen so that a mistake costs
about twenty seconds; the run’s memory is counted by its own allocator and
bounded at 4 GiB, so a list that grows without end stops the build instead of
the machine. A comptime that needs more raises either bound at its
declaration, never in the manifest or on the command line; how it is written
is not decided yet. A run that takes more than a few seconds is reported by
name. ADR-287 D17 and ADR-310 §6 change with it. Decided and specified; not
built.
No Rust code but std’s runs at build time (ADR-321 D9, corrected). 0.0.515
let a Rust crate’s function run where its description said build_time = true;
that column is gone. A description is a judgement, not a derivation, and a
second claim beside it is the same judgement written again. A call that reaches
a Rust crate’s function, directly or through Nikaia code, is refused at build
time (NK1152); std’s Rust half, this project’s own code, still runs.
A dependency’s code runs at build time only on what this build checked
(ADR-321 D8, D9). Compiling build-time code (0.0.513) would have run a
dependency’s functions natively on the strength of the ledger it ships, which
is believed while its sources hash as recorded and is not re-derived; a
package could ship honest hashes beside a false touches = []. Before a Nikaia
dependency’s function runs at build time, the build now derives that package’s
ledger from its sources and refuses where the two disagree; a package without
sources runs nothing then. A Rust crate’s function needs build_time = true
in its description, a column nikaia describe never writes. A Rust
dependency’s build.rs and procedural macros are outside this and not
decided. Decided and specified; not built.
An integer comptime is an open number, typed at each use (ADR-287 D21,
#467). comptime FIB_10 = fib(10) lowered to const FIB_10: i32 = 55;, so
let y: i64 = FIB_10 was refused by rustc, about a file nobody wrote, and
comptime N = 4 * 1024 handed to an i64 parameter the same.
Now, for an integer comptime with no written type:
take_i32(A) and
take_i64(A) in one function both compile.NK1116 at the use, with the
comptime that computed it shown beside it.const has the expression’s type: a call’s result
type, so big(1) and big(3) are both i64. Literals alone still take the
first type that holds them.Seven build_time tests expected the old, value-picked i32 and now expect
the call’s i64. Every bench and example lowers byte for byte as before.
Typed arithmetic at build time (step 5) is left to ADR-321, which compiles
build-time code rather than interpreting it.
Build-time code is compiled and run, not interpreted (ADR-321). The
interpreter that ran a comptime’s calls was a second implementation of the
language: sq(100000) with an i32 result came out as 10000000000 at build
time where the program stops on the overflow, and a std function written in
Rust could not run. A comptime that calls a function is now compiled against
the package’s dependencies, which are linked through one generated dynamic
library, one crate per package, with its results cached; a build-time run is
bounded by a step budget counted in loop turns and calls (NK1152). ADR-287
D16 and D18 and ADR-318 D6 change with it. Decided and specified; not built.
The budget’s default and how a program raises it are not decided.
A mapped file is text wherever text is read (#452). Three spellings
passed the checker and failed in rustc, about generated code. All three now
work:
data[0..<7] slices the mapping’s text, by the same rule as a String’s.data.deref() is the ledger’s fs::Mapped::deref, now a method of the
type itself, so Deref need not be in scope.let c: ref String = data lends the mapping rather than handing it over:
a let that declares a view of what a container holds is written &data,
and data stays usable after it.Every bench and example lowers byte for byte as before. Bytes’ content and
text’s find are still open, and wait on #453’s decision.
fs::exists(path, root) is in std (#454, Part III 17.1, ADR-108 D1).
It was in the specification and nowhere else, so both spellings passed the
checker and rustc refused them. The name goes through the root check the
other path calls use: one that leaves an fs::Root::Dir, by .. or by a
link, throws io::IoError::Outside rather than answering false. The answer
comes from std::fs::exists, so a directory that may not be read is
PermissionDenied and not false. It is sync, one stat. A call with no
root is NK1101, and --trust lists one written with Anywhere. Part I
7.1’s throw example now also reports NK2605, because the call it makes is
described and can fail.
A part an expression arm binds as a view is handed on as it is (#456,
found moving the prover’s terms into Nikaia, #436). SolverTerm::Var(name) =>
given.get(name) ?? id over a match of an owned value binds name ref,
and the call still wrote the compiler’s & in front of it, a &&String
rustc refused. A block arm was already right, found by its statements; an
expression arm’s arguments that name a lent part are now marked by their node
(Checked::views_handed_on). prover_arena.nika’s substitute is back to
the plain arm, and the compiler’s own Nikaia half loses eleven &s it wrote in
front of such parts. Every bench and example lowers byte for byte as before.
The aggressive bounds and overflow walk is Nikaia (ADR-294, #435 step 3).
tools/bounds_walk.nika holds what bounds.rs’s Walk did: the facts kept at
each point of a body, the linear terms and intervals read off expressions, the
expression functions followed into, the bounds on a list’s values found and
then checked (ADR-306 D7), and the questions to the solver whether an index is
inside its list and an operation inside its type. The terms live in the
prover’s TermArena; the solver and the node an expression is are handed in.
bounds.rs keeps the options, the program’s mut parameters and the loop over
the bodies. What is proved is unchanged: the bench and example lowerings with
both removals on and their --bounds reports are byte for byte the same.
comptime marks the name, and an integer comptime is an open number
(ADR-287 D20, D21; #115). The schema in ADR-299 and in Parts II and III was
bound with let app = comptime asset(…), which does not parse; it is
comptime APP = asset("schema.sql"), and there is no comptime expression.
An integer comptime without a written type now takes, at each use, the type
the use asks for, checked against its value; where no use asks, its
expression’s type. The question leaves open-decisions.md. The typing is
decided and specified, not built.
A list of tuple literals builds each tuple’s text (#466). let pairs:
Vec[(String, i64)] = [("a", 1), ("b", 2)] was NK1103, a list of
(ref String, ?), where one tuple literal and a list of plain text literals
were already accepted: the list’s tuples are now built as one tuple is.
A tuple taken apart out of a place is lent, and a list’s position is
converted (#465, found moving the prover’s passes into Nikaia, #436).
let (name, n) = pairs[1] moved out of a borrowed element; it now lends the
tuple, as a one-name let over a place does, and a number, bool or char
part is read out of its view. A part taken out of a lent tuple is a view where
it is a map’s key, so counts[first] after let (first, _) = key is no longer
a &&String. Vec::remove, swap_remove and truncate are described in
std’s ledger: a function that called them was taken to pause, and their
i64 position is converted as insert’s is (ADR-293 D20).
prover_passes.nika’s workarounds go.
The prover’s passes are Nikaia (ADR-294, #436 step 4).
tools/prover_passes.nika repeats the walk until the preconditions, then the
postconditions, stop changing (ADR-269 D15, D17), strikes the postconditions
an exit did not show, and makes the checked entries (D20), what the ledger
publishes (D18), how many calls carry each precondition’s check, and how a
claim no walk reached is held. prove.rs builds the walk, hands in the
compiler’s callbacks and turns the answer into the compiler’s types: 300
lines. Three shapes rustc refused on the way are worked around there and
open in #465. Every bench and example lowers byte for byte as before, and
so do their --bounds reports.
A recursive method hands its function value on whole (#464). #463’s rule
lent a method’s function parameter to a call of the method itself, and each
level of the recursion became a new type, &&&…F, until rustc stopped at
its recursion limit. A call of the function it stands in gets the value
whole, as before 0.0.501.
The prover’s walk is Nikaia (ADR-294, #436 step 4).
tools/prover_walk.nika walks every function, test and bench: what each
binding makes known, the branches and loops, every call by name - an edge of
the call graph, and where the callee has a precondition, what the call shows
of it - each assert, each exit, and another package’s contract, read back
once per function. The compiler hands in only what it alone can answer: the
solver, a name as the language below spells it, the holes of a literal, a
package’s alias, an expression as written and as the checker keys it, and a
condition parsed by its own parser (TermArena::adopt takes its nodes).
prove.rs keeps the passes that repeat the walk until its answers stop
changing, the checked entries and what the ledger publishes: 493 lines, from
2,271. Every bench and example lowers byte for byte as before, and so do
their --bounds reports.
An assert is held in Nikaia (ADR-294, #436 step 4).
ProverState::an_assert proves a claim from what is known before it, makes
it a precondition of the function it stands in (ADR-269 D5, D15) or a
candidate postcondition before a return (D17), refuses a claim about data
from outside (D6, NK1202), shows the values that break it (D8, NK1207) and
otherwise says why it is checked where it is reached (D4). prove.rs hands
it the condition as written and the names it reads. Every bench and example
lowers byte for byte as before, and so do their --bounds reports.
A function value a method took whole is lent on to code that only runs
it (#463). A method’s code parameter is never lent (lends.nika waits on
knowing which entry a method call reaches, ADR-288), so it is an impl Fn
taken by value, and handing it to another method moved it - in a loop,
before the second turn. A call now writes &f for such a parameter where
the callee only runs the code at that place, by its entry or by every method
of that name; &F is a Fn wherever F is. ProverState::carry_back and
precondition_call are methods again.
A call to a function with a precondition is Nikaia (ADR-294, #436 step
4). prover_state.nika’s precondition_call proves each of the callee’s
claims with the call’s arguments, carries one back to the caller’s entry in
the first pass (ADR-269 D15), finds the call breaking it - every time, or for
a state that reaches it (D8, NK1207) - or makes the check the call carries,
and says how the call reaches the callee (D20). Where a body is, Where, is
ProverWhere. carry_back and precondition_call are free functions for now
(#463). Every bench and example lowers byte for byte as before.
A method’s nullable text parameter takes a String? inside its option
(#462). The rule that writes x.as_deref() for a ref String? parameter
looked the parameter up by the callee’s name, and a method call names only
the method: the argument went as &x. Where no entry has the bare name, the
methods of that name are asked, where they agree.
Whether a claim reads data from outside is Nikaia (ADR-294, #436 step 4).
prover_calls.nika’s from_outside walks an expression as
contracts::sync’s walk does and finds a tainted binding or a call of a
source std’s ledger calls untrusted (ADR-010 D2), which makes a claim
NK1202. Every bench and example lowers byte for byte as before.
What the prover reads of a callee is Nikaia (ADR-294, #436 step 4).
tools/prover_calls.nika’s free_function finds a free function of the
program by name - its parameters, whether it throws, whether it hands back
a whole number - and may_throw says whether a call may throw out of the
function it stands in. Every bench and example lowers byte for byte as
before.
Another package’s contract is read back in Nikaia (ADR-294, #436 step 4).
prover_claims.nika’s foreign_shape reads which names a ledger’s
conditions may use and whether the function hands back a whole number, and
foreign_from makes the PreClaims and PostClaims with the assert each
came from (ADR-269 D18); parsing a condition stays the compiler’s. Every
bench and example lowers byte for byte as before.
A let over a list element is a view in a comparison (#461). let held
= xs[at] binds the element lent, and made != held compared a String with
a &String; the comparison reads such a side as it reads any other view.
Text is read through a view that names its unit, and there is no char
(ADR-320, #453). Text stays UTF-8 and a position in it is a byte position.
s.bytes indexes and cuts in constant time; s.scalars and s.graphemes are
sequences. There is no s.len() and no s[i]; s.is_empty() stays. A
position the text hands out - s.find(needle; from:) -> i64?, a grammar’s
match - cuts it without a view, and a number the program makes up needs one
(NK1214). char gives way to scalar and u8, and a '…' is either by its
use (NK1212; a decomposed one that normalises to one scalar is taken, with
NK1213). Bytes is read as a run of bytes. Graphemes are std’s, == and
hashing compare bytes, and normalising is written. Part I 2.2 and the new 2.6,
Part II’s grammar table and Part III’s code table say what; the compiler side
is open in #453.
Carrying a precondition back and showing a postcondition are Nikaia
(ADR-294, #436 step 4). ProverState::carry_back decides whether a
precondition a call cannot show becomes the caller’s (ADR-269 D15),
ProverState::exit whether an exit shows the postconditions standing (D17),
and postconditions_for what a call’s postconditions say of the name its
result is bound to; TermArena::text writes a term as a reader does. Every
bench and example lowers byte for byte as before.
A method’s number keeps nothing alive (#460). tools/buffers.nika gave a
method call’s result the origins of its receiver and arguments, also where
the method hands back an i64: total += into.add(length) put length’s
buffer into total, the buffer went into the frame’s keep, and
known.contains(&length) was a &&String. A method of this package whose
result holds no view hands back its own, as a function already did.
A function takes the parameters the program wrote (#459). One with more
than seven, the receiver counted, is written with
#[allow(clippy::too_many_arguments)]: the lint is a rule about Rust written
by hand, and the warning was about a file nobody wrote.
What the prover asks the solver is Nikaia (ADR-294, #436 step 4).
tools/prover_solver.nika holds the two questions behind a refutation:
refuting_values, the values that show a claim false every time it is
reached, and breaking_values, a state that reaches a claim and makes it
false. The solver stays nikaia-logic, reached through two callbacks that
read the walk’s arena - ask (a SolverAnswer) and model. Every bench
and example lowers byte for byte as before.
A list a function value only reads is a slice (#458). A ref Vec[T]
parameter is written &[T], and the parameter of a function type that read
one was written &Vec<T>, so read(xs) with read: fn(ref Vec[i64]) -> i64
and xs: ref Vec[i64] was refused by rustc. A function type’s list
parameter is now a slice too, as its text parameter was already a &str.
What the prover’s walk keeps across functions is Nikaia (ADR-294, #436
step 4). tools/prover_state.nika’s ProverState holds the preconditions
and postconditions found so far, the calls between functions (with
reaches), the postconditions an exit broke, and which pass the walk is in;
prove.rs’s Prover keeps the program, the ledgers, its findings and the
solver’s copy of the terms. Every bench and example lowers byte for byte as
before.
The claims the prover carries between functions are Nikaia (ADR-294,
#436 step 4). tools/prover_claims.nika holds a precondition’s claims
(PreClaim, with the call it was carried back from as a CarriedFrom), a
postcondition’s (PostClaim), a function’s Precondition and another
package’s contract read back (Foreign); what a failed check says is
PreClaim::failure. Every bench and example lowers byte for byte as before.
What the prover hands the rest of the compiler is Nikaia (ADR-294, #436
step 4). tools/prover_results.nika holds how an assert is held
(ClaimHeld), what a checked entry checks (PreconditionCheck), how a call
reaches a function with a precondition (CallReach, joined by
joined_reach) and the contract the ledger publishes (Published);
prove.rs names them as before. Every bench and example lowers byte for
byte as before.
A number read past a jump is the number (#456, the first of its two
lowerings). let v = m[k] ?? return null over a map of numbers bound the
map’s view, and handing v on - out.insert(name, v) - gave the map a
&i64, which rustc refused; m.get(k) ?? return null did the same. A
number, a bool or a char read through the brackets or a get past a jump
is now read out of the view where it is bound, as a loop binding is, and the
name is the value everywhere below. prover_arena.nika’s int_value goes
back to the plain form.
Where a claim stands at the entry is Nikaia (ADR-294, #436 step 4).
ProverScope gains at_entry, on_the_path, precondition_at,
postcondition_at and pinned_names (the names a breaking state is made of);
prove.rs’s versions are one-line adapters, and RESULT,
PRECONDITION_TERMS and its own is_a_parameter go. Every bench and example
lowers byte for byte as before.
What the prover knows at a point is Nikaia (ADR-294, #436 step 4).
tools/prover_scope.nika’s ProverScope holds the walk’s variables, facts,
entry values and path, with rebind, has_length, not_negative and
blind; prove.rs’s Scope and length_of go. A term is a place in the
Nikaia arena throughout the walk, and nikaia-logic’s copy is made when the
solver is asked. Every bench and example lowers byte for byte as before.
A ?? whose fallback block ends in a jump is a fallback that jumps
(#457). known ?? { s.insert(name) return 0 } handed name over for the
rest of the function - NK2105 at the next read - and was written as
.into() on a block of type !, which rustc refused. The checker asks
whether the fallback leaves, a block ending in a jump included, and the
emitter writes such a fallback as the match a bare jump gets.
The prover’s walk holds its terms in Nikaia (ADR-294, #436 step 4). Every
term prove builds and reads - its facts, claims, substitutions and the terms
prove_terms makes - is in tools/prover_arena.nika’s arena; the solver
reads a copy in nikaia-logic’s arena, kept node for node, so a term has the
same place in both. bounds::into_arena goes: prove_terms builds into the
arena directly. Every bench and example lowers byte for byte as before.
What the language offers is not measured by what this repository calls.
ADR-288 D11 says how a std entry is made - by hand, reviewed, never copied
from Rust’s whole surface - and now says that this is all it says: what std
provides is decided from what the language is for, the coherence of its types
and what other languages count as basic. Fourteen records had cited it, or its
wording, as waits for a program that needs it to leave a capability out
(ADR-019, 042, 105, 150, 247, 282, 283, 284, 290, 291, 295, 299, 302, 312);
each now says not decided here. ADR-286’s Cargo defaults and the owner’s own
until someone needs it in ADR-287 D19 and ADR-293 stay.
The prover’s terms can be held in Nikaia (ADR-294, #436 step 4, the
first of two parts). tools/prover_arena.nika holds an arena of
SolverTerm nodes, with the operations the prover’s walk uses:
constructors, mentions, variables, constant, int_value, substitute
and size. A test checks each one against nikaia-logic’s Arena on the same
terms; substitute builds the same nodes in the same places. The walk does not
use it yet. Two places are written around lowerings rustc refuses (#456).
A view of a list takes a list as its default (ADR-318 D4, #374). An option
xs: ref Vec[i64] may default to a list of numbers or bools, computed
(= three()) or written (= [4, 5]). The ledger records it as [4, 5], and a
call that leaves the option out lends it, &vec![4, 5]. A list the option
would own is still NK1167. A list of anything else is still refused as not
built yet.
An option of a ref type lends what it is passed (#455), as a ref
parameter does (ADR-094 D1). total("c"; xs: [10, 20]) was refused with
NK1106; the checker now accepts the owned value and the emitter writes the
&.
The names a claim reads are Nikaia (ADR-294, #436 step 4). names_in and
is_a_parameter move from prove.rs into tools/claim_names.nika, which
reads the claim the way contracts::sync reads it: every expression in it,
the holes of its literals included. Every bench and example lowers to
byte-identical Rust.
Part I 7.1’s fs::exists names its root. The example wrote
fs::exists(path); ADR-108 D1 lists fs::exists among the calls that take an
fs::Root right after the path, as Part III 17.1 does. The question leaves
open-decisions.md: the record had settled it. std has no exists yet.
Why a check stayed is said by Nikaia (ADR-294, #435). The reasons
--bounds gives for a check it kept (a field, a list of lists, a product of
two unknowns, a call’s result, or no fact at all) are read off the site in
tools/bounds_reasons.nika now, not in bounds_report.rs. The reports for
every bench and example are the same, line for line.
An expression as a term of the proof is Nikaia (ADR-294, #436 step 4, the
first half). lin and claim move from prove.rs into
tools/prove_terms.nika. lin reads a whole number as a linear term, and
claim reads a comparison, &&, || and ! as a formula over those terms.
The Nikaia functions do not reach the compiler’s arena. They append each node
to a list in the order it is made, with its children named by place, and
bounds::into_arena replays the list. The arena therefore gets the same
terms in the same order as before. Every bench and example lowers to
byte-identical Rust.
What leaves a block is Nikaia (ADR-294, #436 step 3). leaves_stmt,
leaves_block and leaves_expr move from prove.rs into
tools/leaves.nika. They decide whether a return, break, throw, ?, a
counted continue or a call that may fail can end what is being proved before
the code after it runs. The walk keeps contracts::sync’s reading of the
tree exactly: the expressions a statement holds, the holes of its literals
(handed in, since only the compiler parses them), and then the blocks that
walk’s companion finds. Every bench and example lowers to byte-identical Rust.
A contract that changed in the breaking direction is said by Nikaia
(ADR-294, #436 step 2). changed_contracts, the NK1208 warning about a
pub function that asks more of its callers or promises them less than the
committed ledger, moves from prove.rs into tools/contract_changes.nika.
Whether one list of conditions implies another stays the prover’s question,
handed in as implies.
How the prover writes a term is Nikaia (ADR-294, #436 step 1). The text
helpers are moved from prove.rs into tools/prove_text.nika:
→ for a precondition under a path;lowered_first and has_a_length.They read the terms one node at a time through tools/solver_terms.nika’s
SolverTerm, which the interval propagation now shares. prove.rs keeps thin
wrappers that pass in the arena and the emitter’s escaping. Every bench and
example lowers to byte-identical Rust.
A binding in scope hides an item of its name (#451). A call through a
parameter of function type, twice(4) in fn apply(twice: fn(i64) -> i64
sync), resolved to a free function twice of the program. The call was then
checked against that function’s signature: twice takes 3 arguments. The
local binding is now the callee, as a let already was.
The interval propagation is Nikaia (ADR-294, #435 step 2). Range, the
closed interval with either end open, and the propagation that proposes a
candidate bound for a linear term are moved from bounds.rs into
tools/bounds_interval.nika. The terms stay in nikaia-logic’s arena and are
read one node at a time through term_of, which needs TermId::index and
Arena::at. The arithmetic is checked i64 rather than i128. A candidate
is confirmed by the solver before it is used, so an overflow loses a candidate
and never makes a wrong bound. Every bench and example lowers to byte-identical
Rust with both removals on.
Two compiler defects the move found are fixed in the compiler (D3):
match whose arms meet at T? because one arm is a T? already is
now Some(…) on its plain arms, as one beside a null was. A block that
ends in null counts as a null arm. Before, the whole choice was wrapped
in .into(), and rustc refused the arms.is_empty on a BTreeMap, BTreeSet, HashMap and HashSet has an
entry in std’s ledger (#450). Without one the call resolved to nothing, and
the function around it lowered as async.A file name is fs::Path, the platform’s own bytes (ADR-319). Part I 7.1
and Part III 17.1 wrote Path and nothing declared it (NK1135); std took
names as text, so fs::walk stopped with NotText at a name that is not UTF-8
and such a file could not be reached. A Path holds the bytes as Unix has them,
WTF-8 on Windows and UTF-8 where names are Unicode. Text stands wherever one is
asked, an f"…" builds one where its use asks, to_text() fails only for a name
the operating system handed over, on every target, and display() shows �.
Part I 2.2 now says a String is always valid UTF-8. Decided and specified; not
built.
A call to an expression function is its expression (ADR-314 D3, #421, in
part). The bounds walk knew nothing of a call’s result, so xs[before(xs.len())]
with fn before(n: i64) -> i64 { return n - 1 } kept its check. A free
function of the file whose body is one return e and that returns a whole
number is now read at the call as e, with the arguments for its parameters,
through a helper over a helper up to a depth of four. A name in e that is not
a parameter belongs to the callee and is not read. Where e overflows, the
callee’s own check stops the program. Not built yet: a callee’s published
ensures from the ledger, across packages (the rest of D3), and D1-D2.
--locked checks nikaia.proofs (ADR-270 D22, #448 step 4). A lowering
now hands its book of answers back to the build instead of writing it itself:
the rustc wrapper, which lowers too, writes nothing, and the command a person
ran writes the file once per package, beside its ledger. Under --locked the
file is not written. The build fails when a question had no entry that checks,
or when the file holds an entry no question asked, with run nikaia build and
commit nikaia.proofs. A cache hit asks nothing and checks nothing, as for
the ledger.
nikaia.proofs is written and read (ADR-270 D4, D19-D21, #448 steps 2-3).
Every question the prover and the bounds walk ask now goes through one place,
nikaia::proofs::ask. It puts the query in normal form, keys it by the
SHA-256 of that form, and answers from the project’s nikaia.proofs where an
entry checks: a certificate is replayed against the normal form, a model is
evaluated. A missing entry, or one that does not check, is searched for and
written in. A project build writes the file beside nikaia.contracts: one line
per question, sorted by key, proved with its certificate, refuted with its
model, unknown with the solver. An entry nothing asked is removed, and a
certificate longer than 4 KiB goes to nikaia.proofs.d/. A build from the same
sources leaves the file byte-identical. The examples’ files are committed.
--locked (D22) does not read the file yet. A dependency’s questions are
recorded in the consumer’s file rather than in the dependency’s (D23).
A query in normal form, and an answer as text (ADR-270 D19, #448 step 1).
nikaia-logic gains Normal: a query rewritten so that the same question has
one form wherever it is asked. >= and > are turned round, the facts are
put in an order by their shape with repeats dropped, and the variables are
renamed v0, v1, … by first occurrence; named maps a model back to the
program’s names. A certificate is found on, and checked against, the normal
form. certificate_text and model_text write an answer as one word,
R(h0,h2,c0:1:1:2,t3), S1[…|…] and v0=3,v1=6, and read it back. This is
what a nikaia.proofs entry will be keyed by and hold; nothing reads or
writes the file yet.
A refused default’s way out is a T? (ADR-318 D7, #374). NK1127 and
NK1152 at a default offered what they offer a comptime: write let
timeout = …, or call it with let instead of comptime. An option was
never a comptime, so that was not something its author could do. At a
default they now say to make the option a T? and compute it in the body. A
std method with no Nikaia body, 30.seconds(), is refused there with
NK1127 naming .seconds() (step 8).
The build cache knows when the compiler’s Nikaia half changed (#447).
ADR-021 D3 keys the build cache on the compiler’s fingerprint, a hash over its
sources. Since ADR-294 part of the compiler lives in nikaia-std/src/tools (the
ledger grammar, absorb_into, the syntax tree), and that was not hashed, so an
edit there served the previous compiler’s output from the cache. The
fingerprint now covers it.
A computed default reaches another package (ADR-318 D3, #374 step 4).
Absorbing a dependency’s ledger qualified an option’s type, at: lib::Point,
but left the default as the library wrote it, Point { x: 1, y: 2 }, and the
consumer’s Rust could not find Point. The default’s type names are now
qualified the same way, lib::Point { x: 1, y: 2 }; a word inside a string or
already after a :: is left alone. A two-package test runs a library’s
at: Point = origin() and scale: i64 = 10 * 3 from a caller that leaves
both out.
A struct is a default (ADR-318 D3, D4, #374). A default whose value is a
struct or an enum variant of literals is recorded in the ledger as its
literal, at: Point = Point { x: 1, y: 2 }, and a call that leaves the option
out receives it. The ledger’s signature grammar reads a brace group as one
piece, so the struct’s commas are not taken for the signature’s. A struct with
a field that owns memory is NK1167 at the default, naming the field, as a
comptime of it is. A list as a default is still refused as not built yet.
A default is refused at the default, and one that owns memory is NK1167
(ADR-318 D4, D7, #374). An option had no span of its own, so a refused default
was pointed at the body’s first statement; ConfigParam now carries
default_span, and NK1127, NK1152, NK1167 and NK1168 stand on the
default. A default whose value is a list, for an option of an owning type
(xs: Vec[i64] = three()), is refused as a comptime of it is: NK1167,
xs is a Vec[i64], which a default can’t hold, with the length the build
computed in the way out, rather than NK1127’s not built yet.
A function the build calls is not never used (#446). A comptime’s
value and an option’s default reach the Rust as literals, so the Rust no
longer called the function that computed them, and rustc’s dead_code
warning reached the reader as Function three is never used about a
function the program used. The emitter now collects what a comptime and a
default call, transitively through the program’s own functions, and writes
#[allow(dead_code)] above each; a function nothing calls is still warned.
A function with options runs at build time, and a default that needs itself
is a ring (ADR-318, #374 step 2). The build-time evaluator skipped every
function that declared an option, and said of it that it comes from a
package, which was not true. It now calls such a function: an option the call
names takes that value, one it leaves out takes its default, evaluated in the
callee’s file with nothing of the call in scope. While a default is worked out
its option stands in the chain of names being resolved, so comptime X = f()
above fn f(t: i64 = X) is refused once with NK1168, the chain is t of
f → X → t of f, rather than with two NK1127s.
An option’s default may be computed (ADR-318 D1-D3 and D7, #374, the first
steps). The grammar took only a literal after the = of an option. It takes an
expression now: timeout: i64 = 30 * 1000, tries: i64 = twice(BASE) over an
item-level comptime, name: ref String = "a" + "b". The default is
evaluated once, by the build-time evaluator, where the function is declared,
and the ledger records the literal of its value, so a call that leaves the
option out, in this package or another, receives the value. A default that
cannot be computed is refused with a comptime’s codes (NK1152, NK1127)
and the option’s name. A default whose value is a struct, a list or a
time::Duration is not built yet: the crossing of such a value and the ledger’s
constant column are the next steps.
1BRC is counted, not timed, and tuned is its own binary. brc.sh now
builds, checks every output against naive’s, counts instructions and
mispredictions with callgrind and prints each program’s length in tokens
(scripts/tokens.py, new). The Rust programs are three binaries
(naive, tuned, gen) instead of one: inside one binary LLVM had not
inlined tuned’s hash-table entry, which cost it 40 instructions a row. The
honest table: Nikaia 300.1 instructions and 1.58 mispredictions a row against
tuned’s 272.2 and 3.70, 618 tokens against 809; the overflow checks cost
1.9. The README and the home page say within a tenth of tuned Rust, which
corrects 0.0.458’s fewer instructions. The benchmark page keeps the method,
the table and where the instructions go, and no longer a diary. CLAUDE.md says
how to measure.
What is proved no longer depends on whether a check is dropped (ADR-306
D14, #390, steps 2 and 3). remove-bounds-checks and remove-overflow-checks
take on and off, both off by default, and say only what is written: the
walk runs at every build, the loop over a list’s own length and every fact the
solver decides together. basic and aggressive are refused with the word
they now are (on). --bounds reports the same proofs whatever the build
writes, and says in its first line whether this build writes them without
their checks.
An option’s default is a build-time value (ADR-318, #374). Part I 5.1 said
a default is a literal, so timeout: time::Duration = 30.seconds() did not
parse and an option’s unit moved into its name. A default may now be anything a
comptime may hold. It is evaluated once where the function is declared, the
ledger records its value, and a call that leaves the option out receives it in
the form a comptime crosses in. A std type the compiler cannot see into
crosses through a new ledger column, constant, which names a constructor and
its parts. A std function runs at build time when it is written in std’s
Nikaia half. Decided and specified; not built.
The README and the home page measure against tuned Rust, in counts. Their
performance sections compared Nikaia with loops this project wrote itself; they
now compare examples/1brc.nika with the single-core tuned entry that maps
the file and skips UTF-8 validation: 300 against 313 instructions a row, 1.58
against 3.61 mispredictions, 618 against 809 tokens of source, no unsafe
against one. Release builds, no LTO on either side, callgrind over a million
rows. The benchmark page records the overflow checks’ cost (1.9 instructions a
row) and how the tokens are counted.
Performance is counted, not timed. On a shared machine the same binaries swap places from one round to the next, so the 1BRC benchmark page, the getting-started guide, the examples’ overview and the roadmap drop their seconds; instructions and mispredictions per row stay. The README’s and the home page’s performance sections follow once they are measured against tuned Rust.
?? after a value that is never absent is NK1211 (ADR-279 D12, #342).
xs[2] ?? return false over a Vec[i64], or x ?? 0 over an i64, reached
rustc as mismatched types or i64: Or is not satisfied. It is refused
now: the left side always has a value, and after a list’s index the help names
the length check. An index into a list of T? keeps its ??, which replaces a
null element; one past the end still stops. And ys[1] ?? "none" over a
Vec[String?] no longer moves the element out of the list: it is lent, and the
answer is a view, as a map’s is.
A forward reference to a comptime in a body is NK1117 alone (ADR-287
D19, #380). comptime D = C + 1 above comptime C = 1 also said D can’t be
computed while the program is built, which named the wrong cause. Tests hold
the other three rows: forward references at item level, an item-level ring
(NK1168), and a body constant reading an item constant.
An assert is a contract, on the README and the home page. “An assert is a
proof” read as an unproved assert does not build, which ADR-269 D4 rejects:
proved where the compiler can, refused where it shows it false, checked when the
program runs otherwise. The headings and the opening sentences now say all
three. Both pages also say that removing index and overflow checks is something
a build asks for, not the default (ADR-306 D14).
The self-hosting count, recounted, on the README and the home page. 30.6 % of the toolchain is Nikaia, 18 625 lines in 61 modules, about 4.8 percentage points a day since September 28. The home page’s bars gain October 4 and run to 40 %; the README’s table, its project figures (1 233 commits, 60 959 lines, 0.0.454) and the roadmap’s two mentions say the same.
ADR-306 D12 and D13 are built: a length is below 2^60 where its elements take
space (#383). len() of a list, text, Bytes, a map or a set whose elements
take at least a byte is a fact the walk starts from. A list of a zero-sized type
is bounded by how it was built, and extend(other) now adds other’s length.
x.len() + 1, a sum and a difference of lengths are proved; a product of two
(#386) is not.
Found building it, and fixed (#444): no i64 or u64 operation had ever
been proved. The solver counts in i64, and the goal term <= i64::MAX,
negated, is 2^63. A proof now states its goal inside ±2^62, a sub-range of the
type. Over the repository’s .nika files, the checks left standing at
aggressive went from 551 to 442 (docs/solver-workload.md §8.5).
The code reached develop in 024e7ab without this heading, with a conflict in
ADR-306 left unresolved: D12 and D13 had been written there at the same time
(0.0.451). This package resolves it to that text, with a note on how it was
built.
The specification states rules, not their status; the README’s version badge
shows the version. Twelve decided, not built yet and Built notes leave
Parts I-III; what is not built is the ADR index’s column, which gains ADR-296
D25’s pub grammar. The version badge’s visible text had stayed at 0.0.437
while its title moved: scripts/badges.py redraws it, and CI now runs it
with --check.
Proving and dropping checks are two settings, and a length is below 2^60, in
the records. ADR-306 D12-D13 (#383): len() of a container whose elements
take space is below 2^60 on every target; a list whose elements take no space
has only the length it was built with. D14-D16 (#390): proving says how hard
the compiler proves, remove-bounds-checks and remove-overflow-checks are
on/off and only choose what is written (off by default); an index or an
arithmetic operation the compiler shows fails is NK1207; the walk’s answers
go into nikaia.proofs. Part III 13.3 and the code table follow; proving’s
levels wait for the measurements. Not built yet.
Two decisions written into the records and the specification. ADR-287
D19 (#380): a comptime at item level may read one declared below it, a ring
is NK1168, and one inside a body is read in written order as a let is.
ADR-279 D12 (#342): ?? after a type that is not T? is refused with the new
NK1211; a list’s index is a T, and over a list of T? the index is a T?
and ?? replaces a null element. Part III’s NK1188 row now says what
ADR-204 D1 says since 0.0.445. The compiler side of both is open in its issue.
--bounds: every check the optimizations may drop, and what became of it
(#389, Part III). What remove-bounds-checks and remove-overflow-checks
proved could be seen only in the lowered Rust. nikaia build --bounds and
nikaia lower --bounds now list every index into a list and every +, - and
* they may drop the check of, by line, proved or checked, and for a
checked one the shape that stopped the walk - a field (#384), a list of lists
(#385), a product of two unknowns (#386), a call’s result (#382), or no fact on
what it reads (#432, #383, #388). At the build’s levels, or at aggressive,
saying so, where it asks for none. On the solver kernels: 36 of 44 indexes
and 23 of 45 operations proved.
A number’s common methods are in std’s ledger (#443). Since 0.0.438 a
method of a number no entry describes is NK1210, and the ledger had the
checked, wrapping and saturating arithmetic but not what a program reaches for
first: a.to_string(), a.pow(2), c.min(d) on an i32, f.floor(),
f.round(), f.max(g), f.is_nan() and the rest were refused where they had
compiled before. The pure methods of Part I 2.2’s number types - text, min,
max, pow, abs, signum, div_euclid, rem_euclid, the bit counts, and
a float’s rounding, powers, logarithms and tests - have entries now, as do
bool::to_string and char::to_string, so a function calling them stays a
plain one.
return ref x has one spelling left: none (ADR-202 D2, #96). The compiler
wrote the & at a return of a place inside a borrowed self, but not inside
a parameter lent to the function: fn of(row: ref Row) -> ref String { return
row.name } was NK1104, and return ref row.name was the only way to say it.
A parameter declared ref T now lends as ref self does. With every root a
lending return can have covered, a ref written there is NK1137, as it is
at a for and at a call. A let bound to a view keeps its own words.
A compound assignment is proved as the operation it is (ADR-306 D5a,
#387). x += d and x = x + d mean the same, but only the second could lose
its overflow check: the first is a statement, not a +. It is now recorded by
the statement’s first byte and proved as x + d with what is known of x
before it, and a proved one is written x = <T>::wrapping_add(x, d). And
xs[i] += d on a list of numbers at a proved position is the value, the
position, one proven::read, the operation and one proven::write - the
element read once, as += reads it; a list of text keeps its checked index.
What it changes on the workloads: nothing yet. The solver kernels’
checksum += … and i += 1 are counters with no bound the walk knows (#432),
and 1BRC’s Stats::add writes fields, of which the walk keeps no facts (#384).
Which types compare when nobody knows them (#379, ADR-204 D1). A known
type is one with a declaration or a ledger entry, std’s or a described
crate’s, and without compares = true it does not compare; a described crate
is std in this, its description being reviewed like code. A name nothing
knows - nowhere::Handle, and a declared type holding one - is no longer
NK1188: it is already NK1135’s, NK2504’s or rustc’s, and the second
refusal named the wrong cause. nikaia describe now reads impl PartialEq for
T (and impl Copy) as it reads the derive, for a trait written without an
argument and a type in the same module.
An if or a match of literals, kept, is declared String (ADR-282
D17, #441). let m = "bob" pushed into a Vec[String] compiled, but
let q = if k > 1 { "x" } else { "y" } was refused with write .clone(),
as if the text belonged to something else. A branch whose every value is a
literal is static text whichever way it goes, and is now declared String
where it is kept, each literal built where it stands; only looked at, it stays
a view.
Found on the way: D17 borrowed the type node it writes from a field, a
parameter or a result declared String, and a program with none - a main
that pushes into a Vec[String] - had no node, so even let n = "ada" stayed
a view there and the push was refused. The compiler makes the node itself now.
A span of time is read back as timeout.in_seconds() (#377, ADR-150 D5).
in_hours, in_minutes, in_seconds, in_millis and in_micros mirror
the five constructors: whole units, cut off (90 seconds is one minute), as an
i64, and a span longer than an i64 counts reads as i64::MAX, as a count
too large to make a span saturates. Rust’s own as_secs and kin stay out of
the ledger. Part III 17.1 shows std::time; ADR-150 D2 says saturates,
which is what the code did, where it said stops. On the way: time::Duration’s
ledger entry lacked copies = true, so a span handed to time::sleep was
gone for the next line (NK2105) although D1 says it copies.
Standard input is read 64 KiB a hop, a pipe’s capacity (#423, ADR-263
§6). io::lines() asked a worker for 8 KiB at a time - BufReader’s size - and
on a pipe that is already full (cat data | prog) the hops were the cost:
4 M lines (60 MB) took 791 ms at 8 KiB and 414 ms at 64 KiB, best of 9, and a
file redirected to standard input 671 against 389 ms. Still one read a hop, so
a slow pipe hands over whatever is there, as before. Not zeroing the buffer
before the read, measured on its own, saved nothing that showed. Whether a
calling-thread path saves what is left stays open.
Four more of #381’s records, made to say what is built. ADR-283 D6 said the
compiler warns where a small view keeps a large buffer alive; it reports it, in
--tethers, and now says so. ADR-251 D4 says ref self and ref mut self are
short for self: ref T and mut self: ref T, the form that can name type
arguments (ADR-288 D17) - std.contracts writes both. ADR-282 D25 keeps its
literal case as a backstop D17 leaves unreached, and says why. ADR-280’s table
gives the three fields’ cost as ADR-211 measured it: a Result 96 bytes wide
where the bare error is 16, the fields alone 88.
What a fold’s step calls directly is written #[inline] (ADR-296 D41,
#433). The step is the loop body over the input and runs once per item, but
LLVM left 1BRC’s Summary::record out of line - hashbrown’s insert path makes
it too big for its heuristic, and no profile setting changed that. The emitter
now finds the functions and methods a fold’s or a par_fold’s step calls, by
name, and writes them #[inline]; nothing else gains the hint. 1BRC, callgrind
over 1 M rows, the same output: 319.1 → 300.1 instructions a row, below
tuned’s 313.
Five of #381’s smaller inconsistencies, settled. RESERVED_BELOW’s comment
said it held only words this language does not reserve; ref, trait,
extern and unsafe are reserved in both, and they stay because after . and
:: a reserved word is a name (ADR-298 D3) - a test now holds the four to the
escape. contracts/keeps.rs said nothing reads the column; three readers do.
ADR-080 D1’s three parts are three checks under two codes, and says so.
ADR-282 §2 cites ADR-283 D19-D21 for Bytes, not ADR-289 D22. ADR-013 D3
says ADR-280 changed how throws lowers.
A method of a number that std does not describe is refused in this
compiler’s words (Part III C.1, #438). a.min(b) on an i64 had no entry in
std.contracts, and the checker took the call as nothing is known: the
function around it was lowered async, and the call went to rustc as written
std’s (a program cannot add one,
NK1209), so a call the ledger does not describe is now NK1210.i64::min, i64::max and i64::div_euclid are in the ledger, as #435’s
interval walk needs them, and so are i32::abs and i64::abs, which Part I 2.2
already names. No program in the repository called a method of a number that
the ledger lacks; tests/number_methods.rs holds the five entries and the
refusal.
An impl on a type the program does not declare is refused in this
compiler’s words (Part I 4.7, Part III C.1, #375). impl time::Duration { … }
came back as rustc’s cannot define inherent impl for a type outside of the
crate, about generated code. tools/types.nika now refuses an impl with no
trait on a built-in type, a std type or a type written with a module in front
with NK1209, and its help names the way that works: a trait of the program’s
own, implemented for the type.
Found writing the check in Nikaia, and fixed in the compiler (ADR-294 D3):
NAMES.contains(name) for a comptime NAMES: Array[ref String, N] and an owned
name was a call no ledger knew - the function around it was lowered async,
and the value went below where a &&str was taken. Array::contains is in
std’s ledger, it is a lookup, and a list of text asked about any text compares
as text (nikaia_std::list::contains). And let t = if c { x } else { return }
had no type: an arm that leaves now gives the other arm’s type, as a match
arm that returns already did.
#376’s program (a program trait’s method called on a std type) already
lowers with its .await; a test now holds it.
What the aggressive bounds walk reads off a body is Nikaia (ADR-306 D4,
D7, ADR-294, #435). bounds.rs’ Shape moves into tools/bounds_shape.nika
as BodyShape: the names bound once to a literal and their value, the names
an unsigned type keeps from being negative, the lists whose every write the
walk sees (the roots) and the names that stand for their elements (the
aliases), with writes_a_value, only_removes, key_root, root_name,
elements_key and tracks. One walk reads them, told which by a Look. What
the program around a body says about a call - each function’s mut
parameters and the methods that take one - is an Around, built once.
--optimization=remove-bounds-checks:aggressive drops the same checks: the
benchmark programs and examples lower to the same Rust. 30.8 % of the
toolchain is Nikaia.
A method on a written number takes the type its use asks for (Part I 2.4,
ADR-285, #417). A number written as a method’s receiver had no type here, so
(-7).wrapping_add(2) was a method on nothing known: the function around it
was taken to pause and lowered async, the language below could not tell
which integer it was, and the negative literal lost its parentheses -
-7.wrapping_add(2) is -(7.wrapping_add(2)) below, a different number. The
receiver now takes the integer type the first use of the call asks for - a
let’s, a return’s, a parameter’s, an assignment’s - as 7 + 1 does, and the
first type that holds it where nothing asks; the type is written on the
literal, and a negative one stands in parentheses. 5.seconds() is written
5i32.seconds() below, where both integer types have the method.
Fewer cuts of the text, and the rest seen to be safe (winnow-grammar#23,
its ADR 24 §12). winnow-grammar 3ca83d3 no longer slices an unbound element
of an indexed run, no longer cuts twice after a scan’s hit to look back for a
\r where the scan has no line_ending, and compares the hit’s byte before
the cut so that LLVM removes the character-boundary check. No unsafe. 1BRC,
callgrind over 1 M rows: 337.1 → 319.1 instructions a row, the same output and
mispredictions; 6 instructions a row from tuned.
A fold’s step changes the accumulator in place (winnow-grammar#22, its ADR
24 §11). fn(acc, m) { acc.record(m) } was lowered as |mut acc, m| {
acc.record(m); acc }, which moved the accumulator - 1BRC’s table of stations
winnow-grammar 761bc1a runs a step
written |acc: &mut _, m| … with a fold that never moves it, and the emitter
writes that form wherever it threaded the accumulator before. 1BRC, callgrind
over 1 M rows: 344.1 → 337.1 instructions a row, the same output and
mispredictions.What a loop’s body does is Nikaia (ADR-306 D4, D7, ADR-294, #125). The
questions the aggressive bounds and overflow walk asks of a body - whether a
break leaves the loop from it, which lists a turn pushes onto exactly once,
and which names a lambda, a task, an overlap branch or a select arm
changes or hands on - move into tools/bounds_body.nika, answered by one walk
told which by an Ask. 30.2 % of the toolchain is Nikaia.
Text bound out of a value the match was lent is read where it is
compared: wanted in Ask::Named(wanted) over a ref Ask is a &String
below, though it types as the view a ref String parameter is (a &str
there), and (x ?? "") == wanted compared a String with a &String. Such a
binding is lent, and the comparison reads it.
A trusted map hashes text with rustc-hash 2’s byte hash (ADR-316 D5,
#419). nikaia_std::hash::FxHasher finished a key with three branches on its
remaining length, which a station name’s changing length mispredicted. Text now
goes through the byte hash rustc-hash 2 uses - a key of up to 16 bytes read as
two overlapping words, the length mixed in - before the Fx step, copied with its
origin and held to the crate by a test. Numbers keep the Fx step alone. With
callgrind, output equal: 1BRC 364.9 → 344.1 instructions and 3.42 → 1.58
mispredictions a row, below tuned’s 3.61; k-nucleotide 1292 M → 1204 M
instructions and 3.26 M → 1.29 M mispredictions; nikaia lower unchanged.
Measured against Fx with an overlapping tail of its own, foldhash, all of
rustc-hash 2 and two variants tuned for 1BRC, in benches/brc/README.md.
A call’s ( is on the line of what it calls (ADR-317, Part I 5.1, #426).
A ( at the start of a line begins what is written there - a tuple, a
parenthesised expression - and never the arguments of the line above it, the
rule ADR-135 D3 made for [. return null if open followed by (x, 2) was
the call open(x, 2): a bool called, the function taken to pause and
lowered async. It is a guard and a tuple, as it reads, and
tools/bounds_basic.nika no longer writes around it.
The basic bounds-check level is Nikaia (ADR-306 D3, ADR-294, #125).
remove-bounds-checks:basic - xs[k] inside for k in lo..<xs.len() whose
body leaves xs’s length alone and does not bind k again - moves into
tools/bounds_basic.nika, with which methods keep a list’s length and whether
a body binds a name again, both of which the aggressive level asks too. The
walk recurses over the tree it is given, so each index it names is one the
emitter finds again. 29.8 % of the toolchain is Nikaia.
An if ending a nested match arm is a statement: a match that is the
whole body of a statement-match arm had value arms, so if a > b {
out.insert(a) } inside one lost its ; and rustc refused the if for
having a value without an else. Vec::clear is in the ledger: a call no
ledger knew made a function that emptied a list pause and lower async.
A line that starts with ( continues the expression on the line before it -
return null if open then (x, 2) is a call of open - which is a question
for the language: #426.
A map’s hasher follows its keys’ trust and shape (ADR-316). There is no
hasher that is best for every map: Fx’s tail branches cost 1BRC mispredictions
on names of varying length and cost k-nucleotide’s keys of one length nothing.
Trust (ADR-010) still decides the class, and the keys’ shape, as the compiler
knows it (a fixed-width type, text of a known length, text of unknown length),
chooses the function within it, never weakening trust. The choice shows only in
--trust and the ledger. A program may name any hasher with hasher: where it
makes the map, and that override is recorded. A shape gets its own function only
once a real program of that shape is measured better. Until then nothing
changes. #419 is narrowed to the first shape, 1BRC’s names. Decided, not built.
A number that does not fit is said in Nikaia, and counted as one
(ADR-285, ADR-294 D11, #125). NK1116 - the number, the type, what that type
holds and the way out - moves into tools/check_numbers.nika. The constant
the checker keeps is integers.nika’s Integer rather than an i128 with a
sentinel one past u64::MAX for a fold that left the 65 bits: such a fold is
marked as one, each place that compared a wide number against a type’s ends
asks fold.nika’s integer_fits, and the overflow at an operation is counted
with integers.nika’s arithmetic (ADR-315’s checked_ underneath). The
build-time evaluator’s i128 adapters are gone. 29.1 % of the toolchain is
Nikaia.
What is left between 1BRC and tuned is written down, and each item is an
issue. benches/brc/README.md lists, in the order they are to be done, the
hash of a name branching on its length (#419), the par_fold loop’s per-item
work (winnow-grammar#22), boundary checks on cuts the generator already knows
are safe (winnow-grammar#23), the stats update (#420), building ADR-314 (#421)
and, last, a grammar over bytes with only text checked (#422) - with what
callgrind attributes to each today, and what was measured out of the list.
fs::map checks UTF-8 with SIMD (ADR-016 D4). checked-text checks each
piece with simdutf8 behind a new simd feature, which nikaia-std turns on:
its fast check for text, its compat half for the offset of a bad byte, so
the verdict and the offset are the standard library’s, held to it by a test
across the vector check’s block edges. ADR-016 had rejected it while the check
was 10 ms of a 140 ms program; on 1BRC it had become the largest thing left
against the loop that does not validate. Over 1 M rows: 390 → 365 instructions
a row, mispredictions 4.28 → 3.42, the same output and the same refusal of a
stray byte; on the clock the same or faster, pinned and unpinned, and the same
on an all-ASCII file.
ADR-314 is decided: what a grammar matched and what a callee promises are
facts of the walk (Option B, not built). At remove-overflow-checks:aggressive
the walk will enter a grammar’s actions knowing what each binding matched - a
digit{1,2} is one or two digits, and a dec[T] over n digits is below
10^n, which is how 1BRC’s whole * 10 will be proved now that its action
reads dec[i32] (0.0.419); a char’s code is a number; a callee’s ensures
holds at the call, and a sync function that is one return e publishes
result == e. Walking a short loop turn by turn, the proposal’s third fact, is
left open: its one site was the loop dec replaced.
A grammar’s entries are entry rule, and pub belongs to the grammar
(ADR-296 D25). pub rule said two things at once: that the program may call
the rule, and - in a language whose privacy is per package (ADR-286 D2) - that
other packages may; a one-file program had to write it for the first. Now
entry rule file says the rule was written to be entered, rule keeps a
helper such as 1BRC’s NAME the grammar’s own, and offering a grammar to
other packages will be pub grammar, for all its entries at once, as a
pub enum offers its variants. An entry is never inferred from a call, which
would make one out of a typo. pub rule is refused with both words in the
message; pub grammar is refused as decided and not built, since a grammar is
entered only from its own file today. entry is a word of the grammar
sublanguage, a name everywhere else. Every grammar in the tree moves, the
toolchain’s own in tools/ included; examples/1brc.nika has neither pub
nor sync left. A test that cut struct Reading out of the example by its old
pub is fixed with it.
examples/1brc.nika writes only what 1BRC needs. Seven syncs, the
comment explaining the word and its place in the header’s list are gone: the
compiler infers that these functions never pause, and the word is a promise to
other packages, which a one-file program has none of (Part II 12.1). So are
pub on its structs and constructors, which a package’s own code does not need
(ADR-286 D2) and which made the build note that two pub functions do not
promise sync. pub rule file stays: it is the grammar’s entry point, the
rule main calls from outside the grammar block. Same output, same 390
instructions a row.
Arithmetic and a conversion can be asked whether they fit (ADR-315,
Part I 2.4). Beside aborting, wrapping and stopping at the limit (ADR-285
D6), checked_add, _sub, _mul, _div, _rem, _neg and _abs answer
T? for i32, i64, u32 and u64 - the number, or null where there is
none, a division by zero included - and checked_i32(), checked_i64(),
checked_u32() and checked_u64() convert out of a type that has values the
destination lacks, lowered as <T>::try_from(n).ok(). There is no
checked_shl or checked_shr: Rust’s check the count and not the bits.
The compiler’s own integers ask instead of comparing (#125):
tools/integers.nika no longer wraps and compares to find out whether a sum
or a product fitted, nor counts an i64 out of a magnitude in fifteen lines,
and tools/fold.nika folds with integers.nika’s arithmetic rather than its
own copy of it - 41 lines fewer. A method called on a bare literal
((-7).wrapping_add(2)) still reaches rustc without a type: #417.
A build has no debug assertions, incremental is a switch, and 1BRC reads
its temperature with dec[i32] (ADR-002 D5, #392). The generated profile is
Cargo’s dev because a Nikaia build has one profile, and it inherited dev’s
debug assertions: debug_assert! in every dependency and the standard
library’s precondition checks in every inlined function. It now writes
debug-assertions = false, with no key to change it. incremental is a key of
[build.<target>] beside opt-level and lto, on where absent; off, a
program compiles as one piece and runs faster, which benches/brc/brc.sh now
writes along with optimization = "…:aggressive". winnow-grammar moves to
bbc8683, where a one-byte literal is compared as a byte in the fast pass, and
examples/1brc.nika reads whole:dec[i32](digit{1,2}) instead of folding the
digits’ text in its action. 1BRC over 1 M rows: 516 → 390 instructions per row
(debug assertions 78, dec 21, the separator 27), against tuned’s 313 and
naive’s 863. benches/brc/README.md now says how every figure on it was
taken, that the clock on this box does not separate differences under 10 %,
that Nikaia’s own optimizations change nothing in this program, and where the
77 instructions left against tuned are.
winnow-grammar moves to 9446f25: a fixed-width run is matched by index
(#392). Its ADR 24 withdraws ADR 16 and carries frames forward, and measures
the two shapes #392 named one at a time. "-"? digit{1,2} "." digit is now a
few byte comparisons and one advance in the fast pass instead of four parser
calls; one search over the file instead of one per line was built, came out
slower (706 against 608 instructions per row upstream, +17 % on the clock),
and is not in it - tuned’s walk back to the last ; would also accept
a;b;1.0, which the grammar rejects. Here, examples/1brc.nika over 1 M rows:
587 → 566 instructions per row (−3.6 %), and on the clock nothing outside the
noise: the one-or-two-digit branch moved from the parser’s repetition into the
action’s for d in whole.chars() rather than going away (mispredicts 4.22 →
5.22 per row). The compiler is unchanged, 382.54 M → 382.56 M instructions
lowering tools/ty.nika. benches/brc/README.md has the tables; a value
made while matching (digit{1,2} handed over as an i32) is the open
question it leaves.
What a build-time expression comes to is Nikaia (ADR-287, ADR-294, #125).
The value the evaluator computes - a number, a truth value, text, a list, a
tuple, a variant or a struct of the program’s own - is
tools/build_values.nika’s BuildValue, and what an operator makes of two
of them and what an index reads out of a list move with it. 29.1 % of the
toolchain is Nikaia.
A type holding one declared in another file compares: whether a type has
== was asked of the types of its own file, so a struct or enum carrying one
from a file beside it lost its PartialEq below and refused a comparison.
The types of the files beside it are now asked too.
A build-time integer is a magnitude and a sign (ADR-287, ADR-294 D11,
#125). The build-time evaluator kept an integer in an i128, which no
Nikaia number is - and which was the reason its move kept being put off.
It is now the shape the tree gives a literal and the fold counts in:
tools/integers.nika’s Integer, with the sum, difference, product,
quotient and remainder, the order, the bit operators on the bits the value
has (ADR-285 D9) and the shifts written in Nikaia. A literal holds no more
than 65 bits, and a step whose answer leaves them is one the evaluator does
not take rather than a number it invents.
A conversion after a value an operator makes keeps the whole value: a
return a - 1 into an i64? whose operands’ type was open went below as
a - 1.into(), the conversion taking the 1 alone. Such a value is now held
in parentheses.
What the checker reads off a callee’s signature is Nikaia (ADR-288,
ADR-294, #125). The arguments a call passes, what the receiver’s and the
arguments’ types bind a signature’s variables to, what a sequence a call hands
back is as a whole (ADR-293 D16) and when a chain over one that pauses pauses
too move into tools/check_calls.nika; what the bindings of a for take
(ADR-105, ADR-152 D4) joins tools/check_types.nika. 28.8 % of the toolchain
is Nikaia.
A nested choice beside a null is Some(…) where its values are made
(Part I 2.3, #410). An arm that meets a null arm is wrapped below; where that
arm was itself an if, a match or a block, the wrap was put around the
whole, and the lowering writes such an arm as one - so if c { if d { a } else
{ b } } else { null } handed its inner branches to rustc as String where
the choice is a String?. The wrap now goes to each branch’s last value. Found
moving the checker’s type helpers into Nikaia (0.0.412), which wrote around it.
A file operation on an idle runtime runs on the calling thread (ADR-263
D1, D2, D4, #378, #320). When main is the one future being polled - no task
alive on this thread’s queue or on the pool, no branch of an overlap or a
select being polled, nothing outstanding on the ring, a worker or the
readiness poller, no timer armed - a read of a regular file, and on the
completion path a write, is made on the calling thread and its future is ready
when first polled. Anything else goes through the runtime as before. D2 now
names the case it missed: an overlap’s first branch finds nothing in flight
because its second has not started, and would have read to the end before it
did. Over 1,000 reads, what a read costs for being async falls from about
2,600 to about 770 instructions at user_parallelism = no and from about
2,550 to about 930 at yes (runtime-cost §7.4). Two test binaries of their
own, one per setting, hold every case D2 names.
What the checker asks of one type is Nikaia (ADR-294, #125). Whether
handing a value out takes it away or copies it (ADR-152 D2), a view of a type
and what a view is of, what a hull holds and what goes into one (ADR-037 D7),
which types are lists, collections or text, the numbers Part I 2.2 offers,
and the one way out a mismatch offers (Part III C.2) move into
tools/check_types.nika. 28.5 % of the toolchain is Nikaia.
A gap found on the way, and written around rather than fixed here: an if
used as a value inside a match arm whose function hands back a T? does
not put a plain branch in the option (#410).
The words the checker’s refusals are made of are Nikaia (ADR-294, #125).
An article, a count, a list of names, the nearest spelling of a name, an
indented block, the {…} groups of a literal, the note on a word another
language reserves (loop, const, macro) and on another package’s entry
that never promised sync (ADR-288 D28), which std lookups lend their key,
and how a boxed member is named (ADR-246) move into tools/check_words.nika.
A clause made a sentence is keep.nika’s capitalised, which already said
it. 28.2 % of the toolchain is Nikaia.
An expression is written back in Nikaia (ADR-269 D2, ADR-294, #125). The
checker’s written and written_pattern - an expression or a pattern as the
source wrote it, with parentheses where the tree needs them and nowhere else,
for an assert’s failure and a refusal’s quote - move into
tools/written.nika. The first of the checker’s own helpers to move. 28.0 %
of the toolchain is Nikaia.
What a build says about pausing, and a test build’s dispatcher, are
Nikaia (ADR-269 D1, ADR-288, ADR-294, #125). The notes on pub functions
that never pause, or pause only where their lambdas do, and do not promise it
(D29); the call path and the unplaced form of the warning that one which could
not pause now can (NK2211, D32); a test’s function name; and the dispatcher
a test build appends to its entry move into tools/modules.nika. 27.7 % of
the toolchain is Nikaia.
A package’s ledger is taken in and published in Nikaia (ADR-100,
ADR-286, ADR-294, #125). What a build does with a package’s ledger as a
whole moves into tools/ledger_ops.nika:
A trait method’s declared entry joins tools/declared.nika (ADR-295 D9).
27.6 % of the toolchain is Nikaia.
What a declaration says is read in Nikaia (ADR-294, #125). The type a
.nika declaration names (Ty::from_ast) and the ledger entry a function’s
declaration makes - its key, pub, its sync word, whether it may fail, its
signature with bounds, options and mut parameters, what its result may point
into, and whether it changes its subject - move into tools/declared.nika.
Every column about the body is still the inferences’ to answer afterwards.
27.3 % of the toolchain is Nikaia.
views’ own reading of a function type agrees with the lowering’s: a
function type holds a view only where its result does (0.0.405), which the
check tools/views.nika runs had kept in its own copy of the old rule.
A ledger is written out in Nikaia (Part III 13.5, ADR-294, #125).
nikaia.contracts, the record beside it of what it was derived from
(ADR-251 D1), a description’s header (ADR-290 D5) and the throws list a
diagnostic quotes move into tools/ledger_text.nika, beside ledger.nika,
which reads the same format back. The files are written byte for byte as
before. 27.0 % of the toolchain is Nikaia.
str::replace is described in std’s ledger. Undescribed, a call of it
was a method nothing describes, and every function around it was taken to
pause (ADR-288’s polarity).
What a declared String is below is decided in Nikaia (ADR-282,
ADR-294, #125). text_tiers’ walk - every value’s kind of text, the
positions it flows into, the fixpoint that decides each one owned, a view or
mixed, and where a value is handed into a mixed position - moves into
tools/text_tiers.nika (1 256 lines), and so does reading a use alias
(Parsed::unaliased). What stays in Rust is writing the answer into the
syntax tree. 26.5 % of the toolchain is Nikaia.
Two compiler defects the move found, fixed in the compiler (ADR-294 D3):
for (a, b) over a set of pairs binds two typed names. A list of
pairs was taken apart and a set was not, so a.clone() resolved to
nothing and the function around it was inferred async.dyn Fn(&'a str), so it took only what lives as long as the struct’s
view, and a call with a local was a borrow rustc refused. Its parameters’
references are elided now, and a struct holds a view through a function
only where its result does.Where each buffer lives is walked in Nikaia (ADR-283, ADR-294, #125).
contracts::keep’s walk - which locals point into which buffers, where each
view leaves its scope, and the keep each buffer goes into, with NK2304
where none can hold it - moves into tools/buffers.nika (1 243 lines). What
stays in Rust is the plan in the types the emitter reads. 24.5 % of the
toolchain is Nikaia.
Two compiler defects the move found, fixed in the compiler (ADR-294 D3):
bool, a char or a tuple does not pause. No
ledger describes i64::clone, so at.clone() was a method nothing
describes, and ADR-288’s polarity made every function around it - and
every caller - async. It is the derived copy, answered as a declared
record’s already was.place: (i64,
String) that the body only reads is a &(i64, String) below, and
m[place] put a second & in front of it, which the map has no Borrow
for.A claim the compiler shows false with values refuses the program
(ADR-269 D8). NK1207 - an assert, or a call to a function with a
precondition, false every time it is reached or for a value the compiler
shows reaches it - was a warning, so that a better prover would never refuse
what an older one built. That is turned round: a program the compiler has
shown to break its own contract does not build, because fewer defects in
production is what the language is for, and what a newer prover refuses was
wrong before it was refused.
The bypass is [build] refuted-claims = "warn", or --refuted-claims warn
for one build: each such claim is a warning with a note and stays D4’s check
where it is reached. It is resolved with the other build options, handed to
the wrapper, part of the cache key at warn, and a word other than error
or warn fails the build. The tally counts the refusals apart from the type
errors (1 claim shown false), and the note that the program stops where
the claim is checked moved to the bypass, where it is true. The tests that
show a check failing at run time build with the bypass, and first show the
program refused without it.
A callee’s precondition is carried back through the call, and a claim false
for a value that reaches it is warned about with that value (ADR-269 D8,
D15). Two gaps between what ADR-269 says and what prove.rs did, found by
trying the home page’s claim that the compiler shows the call that breaks a
contract, with the value that breaks it.
assert of it would be, so
half(x) calling percent(1, x - 2) asks x - 2 > 0 of its callers, and
main’s half(2) is the call told, with x and the assert in percent
it came from; the call in half needs no check. Pass 1 repeats with the
last walk’s preconditions until they stop changing, and a call that can come
back to its function is not carried back, so recursion ends. A failure says
where the claim stands: from assert(whole > 0) in percent.lets, a range’s
counter, conditions the prover read and claims checked before, with no
parameter, call’s result, unread condition or loop that may end early
between them - a model of the facts and the claim’s negation is a state the
program reaches, and NK1207 shows it: for i in 0..<10 { percent(1, i) }
is when i is 0. A break before the call, a parameter’s range or an
unread condition keeps it silent, as before.Two tests named the call inside the function that could not show the precondition; they now name the caller that passed the value, which is D5’s reason for checking at the call. The prover grew by Rust: 22.5 % of the toolchain is Nikaia.
Which count each Shared gets is walked in Nikaia (#125, ADR-294). The
walk contracts::sharing runs before its classes are coloured (ADR-312,
ADR-037) moved into tools/sharing.nika beside the decisions already there:
every handle a unit makes, names, aliases, stores in a field or hands to a
call, joined into allocation classes; a public signature’s and a public
field’s forced atomic (ADR-037 D8); a spawn, a lambda handed to a walk that
runs in parallel (ADR-235 D1) and a callee nothing describes reaching what
they use; a handle handed on by value duplicated (ADR-312 D1); a door over
several locks and a get that copies out of one recorded (ADR-281 D18, D3);
the holes of a literal walked as source (ADR-309 D13). What a written type
means is the compiler’s to say, handed in. The sharing, count and lock suites
pass unchanged.
Moving it found one gap, closed in the compiler (ADR-294 D3): a parameter
handed to a method of the program that keeps it is kept. The walk asked
each candidate’s keeps column, which is what the fixpoint is still filling,
so every method of the package answered keeps nothing - a fallback handed
to Slots::force, which stores it, was lent, and the declaration
&Option<Fallback> met a method wanting the value. A candidate of the package
is now asked through the fixpoint, as a free call is.
And one that was there before it, which the new walk’s allocations brought
out: a literal in an f-string’s hole keeps the type its place gives it.
What the checker records against a node is keyed by the node’s address, and
the checker and the emitter each read a hole as a copy of its own - they met
only where the allocator handed back the same address, so let s =
f"{double(3000000000)}" came out 3000000000i64 for a u32 parameter on main
already. Both now read a hole where it stands in the tree, unless the tier pass
wraps something inside it. 22.7 % of the toolchain is Nikaia.
What a body does with each of its parameters is walked in Nikaia (#125,
ADR-294). The walk contracts::keeps runs before its fixpoint (ADR-094 D2)
moved into tools/keeps.nika: each statement’s expressions classified where
they stand, the holes of a literal included, then the blocks it holds - a
lambda’s body among them, a spawn’s not; an assignment, a return and the
value a body ends in hand a parameter out of the call, through an if’s arms,
a match’s, a block’s last expression and a name a let bound to one; a
struct, list or tuple literal keeps what it is given; ??, ?. and ?
consume what they stand on; a match takes apart what the author did not
lend; a task keeps what it names; a call records what it hands on for the
fixpoint, and a method call asks every entry the bare name could reach. What
needs a type - the parameters, the ones the author lent, each one’s fields,
whether the result is a view - is worked out by the compiler and handed in.
The tools package lowers byte for byte as it did, every & its 12 000 lines
are declared with included, and the keeps and lending suites pass unchanged.
21.8 % of the toolchain is Nikaia.
What a function keeps is settled in Nikaia (#125, ADR-294). The least
fixpoint contracts::keeps runs over the uses each body’s walk found (ADR-094
D2) moved into tools/lends.nika beside the lending rule it feeds: it starts
from what each body keeps on its own evidence and adds what the callees it
hands a parameter to keep, until nothing changes; a callee of the package
answers from the fixpoint in progress, any other from its ledger, where an
absent keeps on a present entry keeps nothing and an absent entry keeps
everything. The body’s walk and writing the ledger stay Rust; the keeps and
lending suites pass unchanged. 21.0 % of the toolchain is Nikaia.
What a body does to its claim to be sync is walked in Nikaia (#125,
ADR-294). contracts::sync’s walk joined its fixpoint in tools/sync.nika:
every function and method of a unit, the calls by name each goes to by
callee_named, an overlap or a select joining on the executor (ADR-292
D16), a code parameter whose lambda decides (ADR-288 D29), the type checker’s
method calls merged in, where each body first pauses itself (D32), the edge
from an implementation to the trait method it implements (D2), and a trait’s
methods and a grammar’s pub rules as leaves (D24; ADR-296 D35). The check of
what a sync function may not call moved with it, Violation included. The
notes a build prints and writing the ledger stay Rust. A field that was never
set - the code parameter a body runs - went with the move: the inference
writes inferred, as it always did.
Moving it found one gap, closed in the compiler (ADR-294 D3): a plain value
lent to a ref T? is put in the option. name_of(Kind::B) for a k: ref
Kind? is Some(&Kind::B); the wrap was never recorded where the argument was
lent, so the value was opened as if it were an option already -
(Kind::B).as_ref(), rustc’s words about a file nobody wrote. 20.9 % of the
toolchain is Nikaia.
What a body reaches is walked in Nikaia (#125, ADR-294). The walk
contracts::touch runs before its fixpoint moved into tools/touch.nika
(ADR-288 D22): every function, method and pub grammar rule of a unit - a
rule over every action of its grammar (ADR-296 D24) - the calls by name each
goes to by callee_named, the type checker’s method calls merged in, a
described callee’s touch set taken over unless it names a parameter, and a
spawn, a dsl statement or an unnamed call leaving the claim unknown
(ADR-292 D3). Writing the answer, only where nobody said and in the order a
set is kept, stays Rust; the touch and overlap suites pass unchanged.
Moving it found one gap, closed in the compiler (ADR-294 D3): an else if
lends its arguments as an if does. The condition is a statement of the
else block, and the checker records the & it owes under that statement;
the emitter wrote the chain under the outer if’s, so else if !has(r.items,
x) handed a Vec to a slice parameter - rustc’s words about a file nobody
wrote.
The walks of throws and touch take their graph as one record with the
type checker’s method calls in it, which keeps every lowered function within
seven arguments - the clippy bound CI holds the tools package to. 20.4 % of
the toolchain is Nikaia.
Which errors leave a function is gathered in Nikaia (#125, ADR-294). The
graph contracts::throws builds before its fixpoint moved into
tools/throws.nika beside the walk and the fixpoint already there: every
throws function and method of a unit, what its body throws itself and whom
in the package it calls, merged with the type checker’s method calls
(ADR-288) - a resolved callee of the package is a call, one of a library
brings what its ledger says it fails with, and one no ledger carries, or a
call that did not resolve, is ? (ADR-010 D1). What one expression reaches is
sync’s answer and stays Rust, handed in; so does writing the ledger. The
contracts and error suites pass unchanged.
Which functions can open a lock is walked in Nikaia (#125, ADR-294). The
walk of contracts::locks (ADR-281 D3) joined its fixpoint in
tools/locks.nika: the doors (D10), what a body opens itself and whom it calls
spawn as the base case (ADR-288),
the free calls by tools/foreign.nika’s walk - a function field’s stored code
as a node of its own (ADR-230 D1), a pub grammar rule walked over every
action of its grammar (ADR-296 D24), and the types a unit declares, so that a
variant reads as a value. Handing in each unit and writing the answer into the
ledger stay Rust; the lock suites pass unchanged.The walk is the first tools file to hand a code parameter to a function of
another file (foreign.nika’s walk), and that showed the guard of ADR-002 D4
comparing the tools one file at a time: read alone, the callee is one nobody
can see, so the parameter read as kept and lowered differently per switch. The
package is lowered as one (ADR-286 D1), so the guard now compares the package
as it is lowered, at both settings. 20.1 % of the toolchain is Nikaia.
What nikaia describe drafts is decided in Nikaia (#125, ADR-294). The
entries a description holds, the crate types they name and what the describer
saw and did not claim (ADR-290 D8, D10) moved into tools/describe.nika, over
the surface tools/surface.nika already reads. The manifest, the crate’s
files, their hashes and the ledger’s header stay Rust.
Moving it found two gaps, closed in the compiler (ADR-294 D3):
?? beside a map read is lent (ADR-279 D7): m[k] ?? none
over a map of lists is a view whichever side answers, as r ?? spare is
over a ref T?. It was NK1185, and the way out it offered was a copy of
the map’s list.let at = m[k] ?? continue binds a reference, and the next map read, a
contains_key and a call that only reads it each wrote a second & -
rustc’s Borrow<&String> is not implemented about a file nobody wrote.19.8 % of the toolchain is Nikaia.
What a grammar matched, what a callee promises and how often a loop turns,
measured and proposed (ADR-314, open). The checks left in 1BRC’s
TENTHS - value * 10 + digit_value(d) over digit{1,2} - stay because the
walk of ADR-271 does not enter a grammar’s actions, reads nothing across a
call (std.contracts has not one ensures), and forgets what a loop changes
at its head. They cost 4.8 instructions a row (1.0 %). Read over the corpus,
these three facts unblock few of the 106 overflow sites the walk does not
prove; an upper bound on a length, values in lists of lists and fields of
self block many more (solver-workload.md §8.4), and ADR-314 §7 leaves them
open. No tool found derives facts from a text grammar; SPARK’s two automatic
rules - an expression body is a postcondition, a short counted loop is
unrolled - are what the proposal transfers. Nothing in the compiler changed.
The encoder a grammar run prints its result with is written in Nikaia
(#125, ADR-250). The statements grammar_run writes into the sub-program that
runs a grammar while the program is built (issue #178) moved into
tools/dump.nika: text through the one escape set, whole numbers, bool,
floats by {:?}, a list, an array or a slice as a run, an enum by a match
with an arm per variant, a struct field by field, and the type that cannot
cross named before a parser is compiled. Reading the program’s declarations
into the shapes it walks, building and running the sub-project and the decoder
stay Rust. 19.7 % of the toolchain is Nikaia.
What the \ in a written literal means is decided in Nikaia (#125,
ADR-250). The one escape table build_time reads a literal through
(ADR-188 D2) moved into tools/escapes.nika, with its two readers - the
build-time decoder and NK1184’s refusal - and the inverse that spells a value
back as a literal. Which numbers name a character is Unicode’s table, handed
in from Rust as char::from_u32. The digits are now read exactly as rustc
reads them, where from_str_radix had read more and less: \x+1 and
\u{+41} are refused rather than decoded, seven digits in \u{…} are
refused as overlong, and \u{1_F600} - an _ between the digits, which
rustc accepts - is no longer refused, the one direction this compiler may
never err in (Part III C.4). 19.5 % of the toolchain is Nikaia.
Which functions stay sync is decided in Nikaia (#125, ADR-250). The
greatest fixpoint contracts::sync runs over the calls it collected
(ADR-027 D1) moved into tools/sync.nika: every candidate starts sync and
loses the claim on reaching a function without it, a callee the graph does not
hold reads as one that pauses, and the answer does not depend on declaration
order. With it, the functions that pause only where their lambdas do
(ADR-244 D2) and the shortest way through the package’s calls to a pause,
breadth-first in key order (D5). Collecting and resolving the calls, the spans
and writing the ledger stay Rust; the tools package lowers byte for byte as
before. 19.1 % of the toolchain is Nikaia.
What a build may read is decided in Nikaia (#125, ADR-250). What the
compiler’s assets decides about a list and a path without touching a file
moved into tools/assets.nika: why a read is refused (Denied) and the
sentence and way out NK1175 gives for each reason (ADR-072 D1, D3), the
allowlist’s own lines (D2), a path that leaves the project root by .. or by
being absolute - decided lexically, as the line reads (D4) - and the entries
nothing read (D8). Reading the files, the lock that records what was read and
the digest stay Rust. 19.0 % of the toolchain is Nikaia.
A lowercase par_fold rule skips no whitespace, and 1BRC is 10.8 % fewer
instructions for it. winnow-grammar is pinned to 28cf576, which generates
a par_fold rule as lexical whatever its name (winnow-grammar#21). Before,
examples/1brc.nika’s pub rule file skipped whitespace at its start and
before every item at user-parallelism = "no" and not in pieces, so the core
count changed the answer: ` b;2.0 was station b sequentially and b in
parallel, and a blank line passed in one and failed in the other - against
ADR-009 D2. tests/one_brc.rs now pins both cases. Measured with the
hand-written halves of benches/brc (#358, taken here): 658 to 587
instructions a row, against 860 for naive and 312 for tuned; on the clock
0.51 s to 0.49 s over 8 million rows. The README says where the rest of the
distance to tuned` is.
What the sharing walk asks of one expression is Nikaia (#125, ADR-250).
The questions contracts::sharing’s walk over the bodies asks without its
state moved into tools/sharing.nika: what a file declares that a slot can
belong to (Fallback::ForeignFile), whether a let is itself the place the
first handle is made (ADR-064 D2), what an unseen origin is said to be
(Fallback::UnseenOrigin), the handle and the type a name in scope has, a walk
that runs in parallel (ADR-235 D1) and the name a call is written with. They
read borrowed fields beside a jump directly, as ADR-275 allows. 18.9 % of the
toolchain is Nikaia, of a compiler that grew by the other session’s ADR-272.
The move found one defect, fixed in the compiler (ADR-250 D3): a map’s read
bound by a let and handed to a ref V? parameter was written as
index::Found, because the lend in front of the read cancelled its *; a
map’s read keeps its * now, and the binding is the Option<&V> the checker
types it as.
What every write into a list keeps in bounds is known of every read from
it (ADR-272, accepted). --optimization=remove-overflow-checks:aggressive
writes a +, - or * the solver proves inside its type as
<T>::wrapping_*, with no unsafe; the checker records each operation’s
whole-number type so that the proof and the emitted operation agree on it.
The walk of ADR-271 learned a bound on the values of every local list whose
writes it all sees, found in two passes and kept only where every write is
proved within it; x % n; the length a loop that pushes once per turn
leaves; constants bound once; and that an unsigned name is never negative.
A bound’s constants come from interval propagation, each confirmed by the
solver. The watch kernel goes from 10.44 M to 9.41 M instructions with
overflow checks on (Rust by hand: 10.69 M), lowering it costs 16 ms more,
and the rest of the corpus is within noise. emit::Needs::of no longer
proves every check a second time. 1BRC gains nothing; solver-workload.md
§8.3 says why.
A borrowed left side of ?? beside a jump is a view wherever it stands
(ADR-275, the owner’s choice).
let s = c.signature ?? return false and let label = c.label ?? continue
over a lent c were refused as a part of a loan taken (NK2106): ADR-259 D1
makes a let keep the answer, and nothing can take a part of a loan. The
left side is now opened, not taken - match c.signature.as_ref() { … } below,
.as_deref() for text - and the let binds a view, as let found = m[k]
does. A view kept past the loan is refused as any view is, with .clone().
tools/lends.nika writes the line directly instead of going round it
through a helper.
Whether a callee lends a parameter is decided in Nikaia (#125, ADR-250).
The rule the declaration, the call and the checker all read (ADR-094 D1, D2)
moved into tools/lends.nika with its two questions: whether a ledger says a
type copies (ADR-252 D4.1), and whether a value moves when it is handed on -
an array as its elements (ADR-152 D2), a function value always (ADR-102 D1),
the numbers, the machine-width names, a Duration and the hulls never. The
walk that fills the keeps column stays Rust. 19.0 % of the toolchain is
Nikaia.
What a method’s name says about text, and which containers text is followed
into, is Nikaia (#125, ADR-250). text_tiers’ tables and the questions on
them moved into tools/tiers.nika: the methods whose result is text of its
own, a view of what they are called on, or an element of it; the methods that
put their argument into a container; the runs, sets and maps whose elements
are positions; and whether a written type is a plain String or a container
(ADR-222, ADR-224 D1). The walk that follows text into positions and the tiers
it decides stay in Rust: its kinds and positions are keys of ordered sets and
maps, which a type this language declares has no order for. 18.9 % of the
toolchain is Nikaia.
Where ??’s answer is only read, a value that is a word by its kind is
copied and everything else is lent (ADR-274,
the owner’s choice, option B.b). A number, a bool, a char, an enum whose
variants hold nothing and a type the ledger says copies are copied; a copy type
made of parts - a struct, a tuple, an array, an enum with a payload - is lent
at every size, as it was until 0.0.376: total(found ?? spare) over a 64-byte
Grid is or(found.as_ref(), || &spare) again, not a copy of both sides at
every read. No size is computed. The record keeps the options looked at: copy
every copy type (A), and three exceptions to lending - the scalars only
(B.a), a word by its kind (B.b) and a size the compiler computes (B.c). What
0.0.377 decided about keeping - a copy type handed on is not taken - stays.
Whether ?? lends a value that copies, prepared for the owner
(ADR-274, open). 0.0.377 taught the
checker that a copy type a program declares copies, and as a side effect a
read position of ?? - total(found ?? spare) - now copies both sides where
it lent them before, although ADR-259 D1 says nothing is copied there.
Measured in optimised Rust: an 8-byte struct compiles to the same instructions
either way, a 512-byte one costs a memcpy per read. The record compares
Rust, C#, Swift, C++ and Kotlin, weighs copying, lending and a size line, and
proposes lending all but the language’s own scalars. Nothing in the compiler
changes until it is decided.
A map of T? values reads one T?, and writing null stores it
(ADR-273, the owner’s decision on the
question open-decisions.md held). Flat, as in Kotlin, TypeScript and Dart,
where the nullable cannot nest - which Part I 2.3 already says of this
language. m[k] and m.get(k) on a map of T? are one T?: a key that is
absent and a key that holds null both answer null, and contains_key
tells them apart (D1). m[k] = null stores null and remove removes - not
Swift’s = nil (D2). The read stays a view, Option<&T> below, through the
new nikaia_std::index::flat (D3). It was an Option<&Option<T>>: a stored
null read as present, and m[k] ?? null and a ref T? argument did not
compile.
A map’s read kept as a T? of its own (D4), for every map: let k:
Kind? = m[1] and kept.push(m[1]) copy the value out where it copies, and are
refused (NK1102) with m[k]?.clone() where it does not. Both reached rustc
as mismatched types. With them, found writing the record’s tests:
ref T? parameter was written
(*get(…)).as_ref(), an Option<&&T>: the read is that option already, and
text is read as str.k ?? Kind::B for a k: ref Kind? of a type this package declares
that copies was refused (NK1185) as a view beside a value; it is the copy,
as for a number.What a file declares, and whether its use lines may name what they name,
is Nikaia (#125, ADR-250). modules’ two questions about one parsed file
moved into tools/modules.nika: the names a file declares into its package’s
one namespace (ADR-047 D1, ADR-144 D2), now read by the same name_declared
the checker’s NK1148 uses, and the four refusals of a use - a path into a
package, one name twice, a package no dependency declares or a file alone
cannot have, and a file beside this one that its package already sees
(ADR-046 D2, D4, D5). Reading the files, the manifest and the ledgers stays
Rust. 18.8 % of the toolchain is Nikaia.
The move found one defect, fixed in the compiler (ADR-250 D3): text a call
made, moved whole into a tuple or a list, was counted by the keep walk
(ADR-209) as a view of it kept outside the loop - named.push((first, c))
put c into the frame’s keep and handed the tuple a reference its String
refused. A struct literal already moved it.
A bound on a list’s values, measured and proposed (ADR-272, open).
ADR-271 §6’s next step, overflow checks dropped by the same proof, was
measured before it was built, and buys nothing: the checks the walk proves
are the ones LLVM already drops. The watch kernel’s +0.7 % with overflow
checks on is one addition, start + at, where start is read out of a list -
not 2 * v + 1 as solver-workload.md §8.1 had it, now corrected. Removing
from the lowered Rust exactly the checks a bound on a list’s values would
prove takes the kernel from 10.44 M to 9.68 M instructions (−9.4 % against
Rust by hand), and with a filling loop’s length to 9.41 M (§8.2). ADR-272
compares how Wuffs, Ada, SPARK, Pascal, Liquid Haskell, Dafny, Astrée,
Clousot, LLVM and the JITs answer the same question, weighs four options,
and proposes the bound inferred by ADR-271’s walk with a criterion to meet
before it is accepted. Nothing in the compiler changed.
The sharing classes are decided in Nikaia (#125, ADR-250). The
union-find over Shared handles, what forced a class atomic, the
duplications and the copies out of a lock, the atomic floor for a slot another
file owns (Fallback::ForeignFile) and reading every handle’s count, reason
and lock off the classes (ADR-037 D7, ADR-238 D2) moved into
tools/sharing.nika as a Slots record. The walk over the bodies stays in
Rust and fills it. 18.7 % of the toolchain is Nikaia.
The move found seven defects, fixed in the compiler (ADR-250 D3):
let found = m[k] then found ?? 0 was typed a ref i64 and refused
for an -> i64 (NK1104): a view of a number beside a value is the number
(ADR-233 D4), as m[k] ?? 0 already was.String handed to a method’s ref String parameter was NK1102
where the same call to a free function is lent: a parameter declared a
view is lent at a method too (ADR-094 D1). A ref written at a method call
stays the program’s own, as Part III’s NK1137 says.Kind? taken from a lent
value, a Count declared in another file — was refused as taken
(NK2106, NK2105).k == Kind::A for a
k: Kind the body only reads — was can’t compare &Kind with Kind
below.T?, m.insert(1, Kind::B), had no
Some(…) written.m[k] ?? maybe, a fallback that may be absent too, lowered to
*index::get(…).or_else(…), whose * took the whole chain; it is
index::or_maybe now, which copies a view of a copy value out as
index::or does.m.remove(k) for a number key had no &: a removal is a lookup.What a read of a map of T? values is - a stored null beside an absent
key - is a question for the owner (docs/open-decisions.md).
What a keep asks of a type, a method and a ledger is Nikaia (#125,
ADR-250). The questions contracts::keep (ADR-209) asks outside its walk
moved into tools/keep.nika: which structs of a unit hold a view, and why a
value of a type cannot go into a handle (ADR-221 D3); the struct of views a
written type names; whether a ledger type holds a view; the methods that keep
what they are given, drop entries, hand back what they remove or hand back a
value of their own; whether a contract takes a keep and the key of a method
that does; the name a callee is written as and the local an expression is
rooted in. The walk that follows views by origin and the plan it decides stay
in Rust, keyed by statement numbers this language has no usize for. 18.3 %
of the toolchain is Nikaia.
The sharing records and the --sharing report are Nikaia (#125,
ADR-250). The closed list of reasons a count stays atomic (Fallback, ADR-037
D8) with each one’s remedy, what one Shared value got (Decision) and all
of them (Sharing), turning every count plain where nothing can cross
(ADR-061 D2), the report’s text, the slot keys, what holds a Shared or a
word, the sentences a decision is explained with, and what Shared[T] and
Locked[T] lower to moved into tools/sharing.nika. The union-find over
handles and the walk of the bodies stay in Rust for now. 17.9 % of the
toolchain is Nikaia.
The move found three defects, fixed in the compiler (ADR-250 D3):
T? compared with a T is refused (NK1102, Part I 2.3). It reached
the language below as expected Option<i64>, found i64; the help hands
over (x ?? fallback) == y and x != null.String? call and null meet at String?
(ADR-207 D2): match e { A => "a", B => kind(b), C => null } was match
arms have incompatible types below, and a match of literals and null
alone was refused as a view returned for a String.Count declared in
another file of the package was not known to copy, so m[k] ?? Count::Atomic
was refused as a view beside a value of its own (NK1185).std’s ledger describes BTreeMap::keys.
The view states are solved in Nikaia (#125, ADR-250). The analysis half
of contracts::tether (ADR-008 D2, D7) - the states one function’s views
solve to (receiver, parameters and result that carry a view, all Borrowed
until keep::infer says a view leaves), whether a pub rule’s result may
point into the text it parsed, and whether an expression makes a buffer of
its own (to_owned, the text form of a literal or a number, a call whose
ledger entry hands back owned text or bytes) - moved into
tools/tether.nika. Writing the states into the ledger, the --tethers
report and the refusal stay in Rust beside the keep plan they read. 17.4 % of
the toolchain is Nikaia, by scripts/self_hosting.py over today’s larger
compiler.
The lowering closes the solver kernels’ gap to Rust by hand, piece by piece,
and an index check a proof shows unneeded is not emitted (ADR-271).
With link-time optimisation, as Part III 13.3 names it for throughput, the
lowered Nikaia now retires fewer instructions than the Rust by hand on all
three kernels; with --optimization=remove-bounds-checks:aggressive the
big-integer kernel retires 34 % fewer. docs/solver-workload.md §8.1 has the
attribution and the tables.
--optimization=remove-bounds-checks:basic|aggressive, and
[build] optimization: basic drops the check of xs[k] in a loop over
xs’s own length that cannot change it; aggressive walks each body with
the linear facts that hold - lets, assignments, ranges, branch and loop
conditions read by polarity, lengths from resize, push, clear - and
drops a check where Fourier-Motzkin proves it inside and the certificate
passes the checker. No level drops a check that is not proved. A proved
read or write is nikaia_std::proven::{read, write}, from the new
crates/unsafe/proven-index (ADR-218). New: src/bounds.rs,
Checked::list_indices, Checked::std_lengths.&[T], as text is a &str: one load
fewer for every read through it (rows −15 M instructions).let mut xs = [] followed by xs.resize(n, v) is vec![v; n], and
Vec::resize is in the ledger. This, not the i64 counters, was 0.0.372’s
+56 % on big integers.as usize,
with no sign test, and is compared with a length in usize; a literal
against a length is written bare. count::of’s sign test is inlined.main that cannot pause starts the runtime on demand
(rt::on_demand): no I/O thread, so malloc takes no locks.mut
parameter (&mut *out), a number whose only uses are a range and an index,
and a literal no i32 holds in a Vec[u32] (4294967295i64).std. The test profile’s
unoptimised one made rustc use its unoptimised RawVec copies - 88 M
instructions no build pays. the_solver_kernels_lowered_against_rust_by_hand
now counts -O and -O with fat LTO, and the proved-checks lowering.-O without LTO, which is linking std at all
(the hand-written Rust with use nikaia_std::prelude::* pays the same, and
any LTO removes it); +0.7 % on watch lists with the overflow checks a
Nikaia crate is built with - the next name for the option (ADR-271 §6).tools/package.rs re-lowered: its read-only list parameters are slices.The solver’s kernels, lowered from Nikaia, against Rust by hand (ADR-270
D8 step 1). benches/solver-kernels.nika holds three kernels a CDCL(T)
solver lives in - combining sparse rows, scanning watch lists, multiplying big
integers - and benches/solver-workload/kernels.rs the same algorithms in
Rust; the_solver_kernels_lowered_against_rust_by_hand (ignored, callgrind)
checks both print the same checksums and counts their instructions.
index::at by as usize leaves 3 %, 28 % and
56 %: the negative-index check on every xs[i] is the gap, and an index
known to be non-negative - a range’s loop variable, or what the prover
shows - does not need it. What remains is i64 loop counters and no
Vec of a given length in std.mut parameter (set(&mut out, ...) on a &mut Vec), and a
number its uses do not type (let variables = 2000;, left to rustc).docs/solver-workload.md §8 has the table. The compiler is not changed.The assert and solver records, consolidated; the solver is written in Nikaia. No code changes.
assert is a
contract, proved where the compiler can, checked where it cannot, published
across packages. 264’s D1-D13 keep their numbers; 266’s D1-D8 are D14-D21.nikaia.proofs (D4, D19-D23); one proof format (D5); the measured workload
(D9); the CDCL(T) architecture (D10-D18). 265’s D1-D8 keep their numbers.ADR-268: the search is free; what a build uses is a committed proof file. By the owner’s decision the solver’s search no longer has to be deterministic - it may use time limits, race engines, share clauses at any moment and keep local caches - on the condition that what one machine proved is never rejected by CI or another machine. No code changes.
nikaia.proofs, committed beside nikaia.contracts: one sorted line per
query the prover asks, keyed by the SHA-256 of its normal form, with
proved and its certificate, refuted and its model, or unknown; long
certificates in nikaia.proofs.d/. Code, published contracts and
NK1207/NK1208 follow from the file only.nikaia build --locked searches
nothing and fails on any difference, naming the query and the fix.docs/solver-workload.md §7).--locked
include it (decided, not built yet).ADR-267: the solver’s architecture, decided before its algorithms. The goal is a universal solver that wins SMT-LIB’s QF_LIA against z3 and cvc5 and then widens; ADR-265 D4 (deterministic, a budget in work) and a small checker are fixed conditions. No code changes.
docs/solver-workload.md, tools in
benches/solver-workload/): the 353 recorded queries and all 1 697 the test
suite asks. At most four variables and 12 atoms; 83 % interval or difference
atoms; 4.2 µs to solve on average, 58 % of it in Fourier-Motzkin; the prover
is 1-6 % of nikaia lower. 92 queries are missed, all an equality with a
coefficient 2 (blank = 2·n, blank = 1): 56 are provable, 36 satisfiable.
z3 agrees with every answer given; z3 and cvc5 were timed on the same files.u32 variables and sparse rows with a dense eight-lane kernel
(D2); an i64 fast path with deterministic promotion to big integers and
fractions (D3); one proof log - LRAT-like clauses, Farkas, Chvátal-Gomory
rounding, splits and floor extensions - and the cuts it admits, with the
checker under 1 500 lines (D4); CDCL(T) as the skeleton with one theory
interface and allocation-free fast paths (D5); model-based theory
combination over an e-graph (D6); a strategy layer switching only at work
counts (D7); a result cache re-checked on every hit, shared lemmas only as a
committed input (D8); the core’s memory layout and where SIMD goes (D9);
SMT-LIB and differential fuzzing as the measuring rod (D10); five build
steps with their criteria, the port to Nikaia after the third (D12).Real
sort.The solver splits where a branch needs it (ADR-265 D4, D5). It took a
query’s disjunctions apart up front, every combination a case: nine != -
nine guards return … if x == k - were 512 cases, past the budget of 256, and
the claim stayed checked at run time.
z' = z + 1 || z' = z + 2 gives z' >= z + 1; a path of twenty such steps
is one refutation instead of 2^20.Certificate::Refuted or
Certificate::Split { disjunction, cases }), over atoms and disjunctions
numbered as the query writes them; the checker numbers them itself and
rejects a split that leaves an alternative out (Rejected::Cases) or names
one the branch has not open (Rejected::NotOpen). The budget counts the
branches looked at, each one elimination: 1024.la_generic lemma per
alternative and a resolution.+1 or
+2 paths, parity), 9 were answered before and 28 now; the one left needs
4096 branches. The 353 queries of the prover’s tests give the same answers.
Every certificate is checked, every Alethe proof - 201 - is valid under
Carcara, and z3 agrees with every answer.The solver’s numbers are i64 (ADR-265 D2, D8). nikaia-logic held its
constants, coefficients, certificate multipliers and model values in an
i128, a width Nikaia does not have (ADR-252
keeps it out). They are i64 now, so a port to Nikaia needs no new number
type. A step that leaves i64 answers unknown (overflow), never a wrong
answer; a program literal no i64 holds is not read, and its claim stays
checked at run time. ADR-265 D2 records the rule and names the way further:
exact integers of any size built from u32 pieces, not a wider type.
valid
under Carcara.tests/queries.rs: a query whose every elimination overflows is unknown.i128, so
x - 1 > 0 cannot overflow where it is checked; that is the lowering’s
Rust, not a number of the language.A proof as Alethe (ADR-265 D6). nikaia_logic::alethe::write writes
the reference solver’s proof of a query in Alethe, for a checker this crate
does not trust. Each assertion is taken apart into clauses of comparison
literals with Alethe’s Boolean rules (and, not_or, and_pos, and_neg,
or_pos, or_neg, not_not; a negated equality through la_disequality);
every choice of one literal per clause is refuted by the solver, its
certificate becoming la_generic lemmas - a bound read off a literal, a
combination, an integer tightening, the final c <= 0; and a resolution tree
folds the choices back to (cl).
valid, the 116 that split into cases among them.
tests/queries.rs runs Carcara where NIKAIA_CARCARA names it.smtlib::write writes != as (not (= a b)); (distinct a b) still
reads back.A contract crosses the package boundary (ADR-266 D4-D7).
assert about the value a return hands back,
shown at every exit of the function, is a postcondition over result and
the parameters, and a caller knows it of the result - let c = clamp(n)
proves c >= 0. Each starts as a candidate and is struck where an exit
does not show it; the walk repeats until none falls. After a check at run
time the claim holds too, and is a fact for what follows.requires, ensures and from in nikaia.contracts,
in the language’s syntax - !(mode == 1) || x - 1 > 0 - and read back by a
consumer with the compiler’s own parser.f, which checks
it, and f__unchecked, the body. A call the prover saw prove the
precondition, or check it where it stands - in i128, with the arguments in
place, naming the caller’s line, the assert’s message and the parameters’
values - takes the second; a function value, a method call, a caller in
another package without a proof, and the language below reach the first.
pub fns and methods have preconditions now; a trait implementation’s
methods do not.NK1208, until it is committed.tests/contracts_across.rs holds two packages end to end.inferred, which nothing would read.The --overlaps report is Nikaia (#125, ADR-250). What every
overlap { … } block was allowed (ADR-050 D3) - under the function that
writes it, the header read off the pairs (the first two branches that may not
run together, and why), and each branch with the half of D6 it falls in -
moved into tools/order.nika, so contracts::order holds nothing but
adapters now. The blocks a body holds are still found by the emitter’s walk,
and whether a branch pauses is still its answer. tests/overlap.rs holds the
report’s text, which nothing did. 18.1 % of the toolchain is Nikaia.
A statement is reduced to an operation in Nikaia (#125, ADR-250). The
walk half of contracts::order (ADR-033) - a let of one name or a bare
expression statement, looked through a catch whose handler cannot divert
(ADR-034), taken apart into the calls it performs, and each call answered
from the ledgers: what it touches, an unknown kind, a failure nobody
catches, a result that may not cross back from the closure overlapping
builds - moved into tools/order.nika beside the verdicts. 17.9 % of the
toolchain is Nikaia.
Found moving it, fixed in the compiler (ADR-250 D3): a ?? fallback that
jumps takes its takings with it (ADR-213 D4), as a branch that leaves
does. let c = f(k) ?? return Refused(k) hands k over only on the path
that leaves, and a use of k on the next line was NK2105.
tests/defects_found_running.rs holds it.
Whether statements keep their order is decided in Nikaia (#125,
ADR-250). The verdict half of contracts::order (ADR-033) - a statement
reduced to an Operation or the reason it could not be (Accounted, with
the sentence a report prints), and whether two operations, or a whole run of
them, may overlap (verdict, group_verdict, group_of: a data dependency,
a shadowed name, two touch sets meeting on a write, two names for what may be
one destination) - moved into tools/order.nika. The walk that reduces a
statement stays in Rust for now. 17.3 % of the toolchain is Nikaia.
Whether a value may cross a thread is Nikaia (#125, ADR-250). The
verdict NK2501, NK2502 and NK2503 stand on - plain data, containers
answered by what they hold, the lock family whose answer is the destination’s
(ADR-045 D2, D3; a shared count into code nothing describes, ADR-061 D1), a
described type’s own word or its fields walked transitively (Group B), and
which of a task’s bindings are held across a pause (ADR-055 D6) - moved from
contracts::send into tools/threads.nika. 17.0 % of the toolchain is
Nikaia.
Found moving it, fixed in the compiler (ADR-250 D3): BTreeSet::remove is
described. Nothing was, so a walk that marks a name and takes the mark off
again was taken to pause and lowered async. tests/defects_found_running.rs
holds it.
The refused part and its field are answered one at a time
(refused_part, refused_at): a nullable pair is not a type Part I 2.3
writes, and the parser says so on purpose.
A claim is carried back to the entry and becomes a precondition
(ADR-266 D2, D3, D8 within a package). The prover keeps each whole number’s
value at the function’s entry - y after let y = x - 1 is x - 1, and x
after let x = x + 1 the parameter plus one - and the branches and guards on
the way. A claim that then reads only the parameters is a precondition,
path → claim, and its callers prove it: let x = x + 1; assert(x > 3) asks
each call for x > 2, and g(3) leaves no check behind.
match arm, a call’s result,
a mutable binding, or a condition past a fixed number of terms. The claim
stays a check where it stands.NK1207).assert it came from (D8): f’s precondition mode == 1 → x - 1 > 0,
assert(y > 0) in f, carried back to its entry.nikaia_logic::Arena gains substitute and size.A claim the facts rule out is a warning with values (ADR-264 D8; step 3
of ADR-265 D8). The solver answers refuted with a model - found by
back-substitution through its eliminations, each value as near to zero as its
bounds allow, and checked by evaluating the query. The prover warns, NK1207,
where it proves, with a checked certificate, that what is known before a
claim rules it out: the claim is false every time it is reached, and a model
of what is known gives its names’ values. The same at a call that breaks its
callee’s precondition every time, with the parameter’s value as the call
gives it. The claim stays a check; --asserts says why, with the values.
tests/prove.rs holds
that too.Two of the compiler’s messages are Nikaia (#125, ADR-250). A backend
mismatch at a boundary said as the stale ledger it is (ADR-100 D6, ADR-237
D2) - which package or described crate to name, and what the message and
its notes become - moved into tools/boundaries.nika; NK2202, a sync
promise something in the body breaks, into tools/render.nika beside the
layout it is written in. 16.7 % of the toolchain is Nikaia.
Found moving it, fixed in the compiler (ADR-250 D3): str::splitn is
described. Nothing was, so a function that split a name once was taken to
pause and lowered async; its count is an i64 the lowering converts, as
chunks’ is (ADR-212 D5). tests/defects_found_running.rs holds it.
A proof comes with a certificate, and the prover checks it (ADR-265 D5,
D6; step 2 of D8). Fourier-Motzkin hands back, for every case of a proved
query, the derivation of its contradiction - the case’s own bounds, positive
combinations, and integer tightening - kept to the steps it needs.
nikaia_logic::verify computes the query’s cases itself, so a certificate
cannot leave one out, and replays each step. The prover checks every
certificate before it leaves a check out; one that does not check keeps the
check, and --asserts names it as a fault of the compiler.
smtlib::write gives any query as a QF_LIA
script - unsat where it is proved - and smtlib::read takes the part of
SMT-LIB 2 the sorts cover back. Neither is on the prover’s path.unsat, each of the 174 it leaves
unknown is sat.A finding’s layout is Nikaia (#125, ADR-250). The shape every message a
reader gets has - the headline, --> file:line:column, the lines with each
named place underlined (^ where the error is, - where something explains
it, ... between lines that are far apart), then the notes and the help as
sentences - moved from the compiler’s diagnostics into
tools/render.nika. A span is in bytes and a column in characters, so the
mapping between them is written there too; tests/diagnostics.rs holds it on
a line with ü before the place. 16.6 % of the toolchain is Nikaia.
views is Nikaia (#125, ADR-250). Where a naked view parameter’s view
ends up - a field of the subject, a struct the function builds, its result, a
task - and so whether it is refused (NK2302) or written as a view of the
buffer the subject or a struct parameter names (ADR-008 D1) moved into
tools/views.nika. Whether a let’s value is a buffer of its own is still
contracts::tether’s answer, handed in. 16.3 % of the toolchain is Nikaia,
up from 14.5 %.
Found moving it, each fixed in the compiler (ADR-250 D3), each held by a test
(tests/defects_found_running.rs, tests/sysroot.rs, tests/modules.rs):
Symbol - a described type whose ledger says
copies - or one of the unit’s own Copy types, as a number already was.
for name in names over a ref Vec[Symbol] handed resolve a view and
was refused as a mismatch.comptimes were bound - TEXT from ty.nika was
NK1117 in views.nika.Vec::insert and Vec::join are described. Neither was, so an i64
position reached rustc unconverted (it is a count now, as chunks’ is,
ADR-212 D5), and a function that joined a list of text was taken to pause
and lowered async.params[i].0 was
untyped, so its comparison with a lent String reached rustc as String
== &String. Typed, r.0.len() over an overlap’s reads resolves, and
main’s error is named io::IoError rather than the catch-all.match over a name a match lent, or over a place reached through the
brackets of a view, reads it as the view it is: match target {
Expr::Variable(name) => … } for a target bound out of block.stmts[i]
handed on a &Symbol; the number it binds is copied out now.NK1102 and .clone() as the way out, as a view of text
already was (ADR-225 D2): for p in found { out.push(p) } reached rustc
as expected P, found &P.The variant a result is stored in is Destination::Returned: one named
Result made rustc print std::result::Result in full, and a stale
description’s Result mismatch was no longer recognised as one.
types is Nikaia (#125, ADR-250). The check that a file declares a
name once (NK1148), that a bound names a trait (NK1135) and that every
written type is one something declares (NK1135, NK1158 for a &mut
away from C, NK1182 for an Array[T] with no length outside a parameter)
moved into tools/types.nika. What the file declares as types and what an
impl’s target makes a type parameter are contracts’ answers, handed in.
14.5 % of the toolchain is Nikaia, up from 13.8 %.
Found moving it, each fixed in the compiler (ADR-250 D3), each held by
tests/defects_found_running.rs:
let first = seen[name] ?? continue is the Some of a get
below, a &String, and the checker read it as a value of its own - so
first == kind for an owned kind reached rustc as &String == String.
Such a binding is read as a view now, as a for binding is.continue or break takes its takings with it,
as one that ends in return already did (ADR-213 D4): kept.push(name)
then continue was refused with NK2105 for a read after the if that no
path which pushed reaches. A name from outside the loop is still refused,
by the loop’s own rule.The prover asks nikaia-logic (ADR-265 D1-D4, step 1 of D8). A crate with
no dependencies now holds the logic layer: terms by u32 id in an arena
(TermId, Arena), a self-contained Query of facts and a goal, an
Answer of proved or unknown with the reason, a Budget counted in cases
and bounds, the Solver trait, and Fourier-Motzkin as the reference solver.
prove.rs is the frontend: it walks the program and builds terms, and asks
one query per claim. What a program sees is unchanged, and the suite holds it.
assert could be “proved”: let x = x + 1 was taken
as the fact x = x + 1, a contradiction from which every later claim
followed, so its check was left out. The same for for i in 0..<i. A value
or a range that reads the name it binds now adds no fact about it. The fault
was in the prover since the MVP; tests/prove.rs holds it.nikaia-logic is in the compiler’s fingerprint for the cache key, in the
floor check, and counted as toolchain Rust by scripts/self_hosting.py, so
moving code out of crates/nikaia/src does not raise the share.crates/nikaia-logic/tests/queries.rs asks the solver directly, f(5)
against x > 1 among them.ADR-266: a written assert is a published contract. The owner’s answer
to what crosses a package boundary: no clauses in a signature - the assert
the author writes is the promise, and a guard is only an observation, read as
absent by another package. A claim is placed by computing it back to the
entry (weakest precondition: a let substitutes, an if becomes an
implication) or forward to every exit, so a claim after let y = x - 1 is a
precondition about x. The ledger gains requires, ensures, inferred
and from; a contract that changes in the breaking direction, decided by the
solver against the committed ledger, warns its author; a function with a
precondition gets a checked and an unchecked entry. Amends ADR-264 D5. Not
built.
ADR-265: the prover is a frontend, a logic and a solver. The owner asked
for a layer between the prover and its solver, so that the solver can serve
other problems and languages, and for a check that nothing in it stands in
the way of the plan: a solver written in Nikaia that outdoes others on
parallelism and performance. The record cuts prove.rs into the walk over
the tree and a dependency-free nikaia-logic crate, and fixes the promises a
later solver needs: terms by id in an immutable arena, self-contained queries
without SMT-LIB’s assertion stack, answers that depend on a budget counted in
work and never on time or threads, certificates so that a fast solver need
not be trusted, and SMT-LIB 2 only as an export and import. Within a query,
parallelism splits into deterministic cubes or shares learned facts at
epochs fixed by work counts; a certificate is per theory and small, with
Alethe as an export. Not built.
What changed is said in Nikaia (#125, ADR-250). project’s
changed_lines - the lines of a ledger a --locked build or a stale check
finds different from the committed one, each under its entry - and the line
half of difference - what an output test prints between the output it
expected and the one it got, two lines of context around each run - moved
into tools/diffs.nika; whether the bytes are text at all is still asked
in Rust. tests/diffs.rs holds both, which nothing tested directly before.
13.9 % of the toolchain is Nikaia, up from 13.7 %.
A claim the compiler cannot prove is checked when the program runs
(ADR-264 D4-D7). prove.rs no longer refuses an assert it cannot prove:
the claim is held AtRunTime with the reason, and the emitter writes the
check it already wrote for a test’s claim. NK1202 is left to D6 - a claim
about data from outside the program, which a guard checks instead.
(if !(total > 0) { assertion_failed(…) }, percent(done, total)).1, the
callee’s claim with the call’s arguments in place of its parameters, so a
failure names the caller’s line. Two calls that read the same carry the same
check. Where a call’s argument is not one the prover reads, or the function
is handed on as a value, the body checks it instead. NK1203 and NK1204
are retired.--asserts says why (D7): each claim checked at run time names the
reason the prover gave up, a precondition says how many calls check it, and
the heading counts the claims checked at run time.An assert is a contract: proved where the compiler can, checked where it
cannot (ADR-264). The two records on test and assert are withdrawn and
replaced by one. The first made an assert a claim the compiler proves,
refutes or checks at run time; the second took the run-time row away outside
a test, so a claim the prover could not read refused the program. With a
prover that reads linear whole-number arithmetic and nothing else, that kept
every invariant about fields, matches, lists and calls unwritable - including
the ones the compiler’s own Rust states with unreachable!, which self-hosting
has to carry into Nikaia as something. ADR-264 keeps everything else of both:
test blocks, nikaia test, output tests, the pure condition, preconditions,
the refusal of a claim about data from outside the program (a guard’s job),
and the prover. What changes is D4: a claim that is not proved is checked
where it is reached, and stops the program when false; a proof only removes
the check. --asserts (D7) is how a reader finds the claims still checked.
NK1202-NK1204) -, D7’s reasons and
count, and D8’s refutation. Part III 14.2, the index and the code’s
citations say so.A parse error’s reading is Nikaia, the text half (#125, ADR-250). What
the parser’s better readings look back at - the token before the caret, the
quote, the bracket and the block comment never closed - and the notes a
refusal gets from what a reader sees on its line (a ??’s fallback, a let
taking apart a nested tuple, & for ref, an effect on a lambda), with the
split of a written message into its headline and the rest, moved into
tools/parse_notes.nika. It answers in characters, and the compiler turns a
position into the byte a span counts (tests/parse_error.rs holds an
unclosed quote after umlauts). 13.7 % of the toolchain is Nikaia, up from
13.3 %.
How a message is set down is Nikaia (#125, ADR-250). diagnostics’
said - a message as a sentence, capital and full stop (Part III C.2) -,
headed - error[NK1102] - and word_in - the column a finding’s word
stands at on its line, and how wide it is - moved into
tools/rustc_words.nika beside the rewriting of rustc’s words. Every
message the suite renders, the error corpus’s among them, is what it was.
13.3 % of the toolchain is Nikaia, up from 13.2 %.
A rustc message is put in the program’s words by Nikaia (#125,
ADR-250). What diagnostics does to a message the language below wrote about
the generated Rust - Rc<T> and Arc<T> back to Shared[T] with the brackets
matched, the lock’s two shapes back to Locked, a trusted map back to
HashMap, Option::map back to the ?. the program wrote (ADR-056 D1,
ADR-057, ADR-189), the note that names something a .nika file cannot reach,
and the expected A, found A that marks this compiler’s own defect (D2) -
moved into tools/rustc_words.nika. tests/diagnostics.rs holds a nested
hull, a name that only ends in Rc<, and the lock and map. 13.2 % of the
toolchain is Nikaia, up from 13.0 %.
The specification’s blocks are read in Nikaia (#125, ADR-250). The text
half of specbook - a page’s ```nika blocks, whether the page is
discussing a refusal in one, whether an elision stands where code would, the
programs a block can be read as, and the line a report names it by - moved
into tools/specbook.nika. Reading the pages and taking each reading through
the compiler stay Rust. 13.0 % of the toolchain is Nikaia, up from 12.7 %.
error[NK - was written in capitals, so it
never matched. Part I 7.1’s // without throws: error[NK2701] is a page
discussing a refusal, and tests/specification/EXPECTED.txt says so now -
the one line of it that moved. char::is_alphabetic has an entry in
std’s ledger, which the elision test asks.sync reads a body off the walk in Nikaia (#125, ADR-250). Both of
contracts::sync’s walks - what a body reaches for the inference, and what
breaks a promise for the check - read the list tools/foreign.nika’s walk
hands back, and resolve each call by name with calls::callee_named. The
walk says where an overlap and a select stand (Seen::Joins, which join
on the executor and so pause, ADR-163 D1) and gives a spawn and an
Opaque their statement’s span, and it says what an expression is before
the holes inside it, which is the order a refusal names the first pause in.
joins_on_the_executor and called are gone with the walks: seven readers
of the tree share one walk now. Every ledger and every refusal the suite
meets is what it was.
What a body touches is read off the walk in Nikaia (#125, ADR-250).
contracts::touch’s own walk is gone: it reads the calls of a body off
tools/foreign.nika’s walk, which now says where a spawn, a dsl
statement or a call of something that is not a name stands (Seen::Opaque,
Seen::Spawn), and resolves each call by name with calls::callee_named -
callee_of with the expression already read, the one resolution every
analysis shares (ADR-028). Six readers of the tree share one walk now, and
foreign.nika’s copy of calls.nika’s written_path is gone. Every ledger
the suite writes is what it was.
What a body throws is walked in Nikaia (#125, ADR-250). contracts::throws’
walk - what a function names in a throw, whom in its package it calls, and
what a catch that does not pass its error on keeps from it (Part I 7.1,
ADR-157 D2) - moved into tools/throws.nika beside the fixpoint it feeds
(0.0.321). What a call resolves to stays the compiler’s single answer
(ADR-028), handed in per expression. The set of expressions the Rust kept by
address to say caught is a mode the walk carries into a guarded half, and
the handler blocks it kept by address are the ones a catch hands its
handler to, which is the only place a handler stands. Every ledger the suite
writes is what it was. 12.7 % of the toolchain is Nikaia, up from 12.2 %.
Every name a statement mentions is Nikaia (#125, ADR-250).
contracts::send’s names_used_in_stmt - which of a task body’s names are
held across a pause (ADR-055 §2 D6) - asks tools/names.nika’s
names_in_statement, which names_in_block is now made of, instead of
walking the statement with sync’s visitor and asking names_in of every
expression it met, each one walked again inside the last.
What a body keeps is asked of the walk in Nikaia (#125, ADR-250). The walk
of tools/foreign.nika names every bare name and every spawn it meets where
it is asked to (with_names; a build’s every-file walk does not pay for
them), and starts from one expression as well as from a block or a program.
contracts::keep’s and contracts::keeps’ own walks of names are gone: five
readers of the tree share one walk now. Every ledger the suite writes is what
it was.
mut parameter was not typed at all - the &mut is the compiler’s to
write, and the check stopped there, so a Walked handed to a
mut found: Vec[Seen] was accepted and rustc refused the file. It is
NK1102 now, as at any other position (tests/defects_found_running.rs).Every name an expression mentions is Nikaia (#125, ADR-250).
contracts::order’s names_in and names_in_block - what decides whether
two statements may change places (ADR-033 D9), and what contracts::send
asks of a task’s body - moved into tools/names.nika, whole: still total,
with an arm for every variant of the tree and no else, which Nikaia’s
NK1151 holds the way Rust’s exhaustiveness held the Rust. Every ledger and
every refusal the suite meets is what it was. 12.2 % of the toolchain is
Nikaia, up from 11.8 %.
locks reads a body with the walk in Nikaia (#125, ADR-250). The walk of
tools/foreign.nika takes one block as well as a whole program
(seen_in), and contracts::locks reads the free calls of each function’s
body and each grammar action off it - its own walk and free_call are gone.
Three readers of the tree now share one walk (foreign, --trust, locks);
every ledger the suite writes is what it was.
--trust reads the program with the walk in Nikaia (#125, ADR-250). The
walk of tools/foreign.nika meets calls as well as paths now (Seen), and
for a call by name it says what the call hands a parameter called root
where the callee has one. contracts::trust reads its reasons and its roots
off that list, and its own two walks - sync::walk_calls and roots_of -
are gone: one walk of the tree, two readers. 11.8 % of the toolchain is
Nikaia, up from 11.7 %.
Found moving it, each fixed where it was (ADR-250 D3):
a_calls in two files were lowered as one, and a call of the one met
the other’s parameters. lower-std refuses a name declared in two files
with the project’s sentence now (ADR-047 D1, modules::declared_once), and
tests/sysroot.rs holds it.root_at(name) for a root_at: fn(ref String) -> i64 wrote root_at(name)
below, a String where the closure takes a &str. What a code parameter
takes as a view is lent to it now, as a declared function’s argument is
(tests/function_types.rs).dsl is Nikaia (#125, ADR-250). The rest of the compiler’s dsl moved
into tools/dsl.nika beside the scan of a body that went first (0.0.248):
whether a statement’s holes are deferred, the shadow type’s name, the shadow
types a program needs, the functions that take a statement’s parameters, and
the check of every call that hands them over (NK1109, NK1112, NK1113).
The compiler’s dsl is the calls. 11.7 % of the toolchain is Nikaia, up from
11.1 %.
Found moving it, each fixed where it was (ADR-250 D3), and held by
tests/defects_found_running.rs:
?? return at the end of a line took the next line as its value. In a
function that returns nothing, let x = y ?? return and then
names.push(…) lowered to return names.push(…) - the push ran only where
the return did. A return inside an expression hands back what stands on
its line now, ADR-135 D3’s rule for an index.a ?? b where b may be null too was read as a T, and its fallback
converted into what the option holds, which an option is not. It is the
first that has a value, or null - a T?, lowered to or_else
(Part I 3.5).ref String handed the list’s
own contains a &str where it takes a &String. It is compared element by
element now (nikaia_std::list::contains), and the copy ledger.nika wrote
to get past it (ref name.clone()) is gone.collections::BTreeMap::remove had no entry in std’s ledger, so a
call of it read as one that may pause, and so did every function around it.tests/range_spelling.rs reads only a for head for the old range spelling:
a message that says in and names ...args is text.
The first walk of the tree in Nikaia (#125, ADR-250). foreign’s walk -
every qualified name a unit writes, for NK2504/NK2505 and for nikaia
describe - moved into tools/foreign.nika: every item, statement and
expression, read the way contracts::sync’s walk reads them, a hole of a
literal included. What a literal’s holes are and what a file’s alias stands
for are the compiler’s to answer, handed in as two functions (holes_of,
unaliased), because a template’s holes are parsed out of its text. 11.1 % of
the toolchain is Nikaia, up from 10.6 %.
let name = f(s) left
name.contains("::") unresolved, an unresolved method is read as one that
may pause, and the function came out async with nothing in it that
pauses. The call hands back what the parameter’s type says now
(tests/function_types.rs).A signature is written in the ledger’s spelling by Nikaia (#125, ADR-250).
contracts::spelling moved into tools/ledger.nika as spell, beside
spelled_signature that reads the spelling back, so both directions of
ADR-251 D4 are one file of Nikaia; the Rust module is gone. Every ledger the
suite writes is byte for byte what it was. 10.6 % of the toolchain is Nikaia
(scripts/self_hosting.py), up from 10.2 %.
at + w.len() as i64 < n, which Rust reads as i64<n> and refuses. The
emitter parenthesised a conversion standing there alone and not one ending
an operation; it asks how the operand’s text ends now.A caret on a token that is not a word stays where the parser stopped. The
renderer looked for the token found as a whole word, and asked a } or a
space for word edges it cannot have; failing that, it took the first match on
the line, which put the caret of f"x } y"’s refusal in column 1. Word edges
are now asked only where the token begins or ends with a word character, and
the stray } of an f"…" is refused on the brace itself rather than after it.
The emitter builds an f"…" from its parts (#107, ADR-262 D5). The format
string is the text runs as written and {} or {:spec} per hole
(emit::format_of); the body as written is gone from the tree
(Expr::LitInterpolated { parts }), and with it emit::interpolation, the
scanner that found holes in text. A hole carries the byte its { stands at,
which is what the tier pass’s wraps are keyed by now - per hole, where the
hole’s text was shared by every hole that read the same. The Rust emitted for
benches/template.nika, page.nika, jumps.nika, awaits.nika and
examples/1brc.nika is byte for byte what it was, so they measure the same.
The grammar parses the holes of an f"…" (#107, ADR-262 D1-D4). f"…"
is a rule of text runs and holes, and a hole is the ordinary expression rule;
the tree holds the parts (Expr::LitInterpolated { text, parts }, an
FPart each), so the checker, the emitter, the tier pass and the build-time
evaluator read the holes the grammar parsed rather than parsing the text again.
Every walk sees a hole as a child of its literal - which a walk written in
Nikaia needs, having no parser of its own.
f"{greet("Ada")}". \" inside a hole is refused with that
sentence; examples/escaping and about fifty lines of tests moved to plain quotes,
and Part I 2.5 says so.f"{let}" is
expected expression at the let; a { the string ends inside of and a
} with no { are named, as the emitter named them.A file read stops doing work nobody asked for (#320, ADR-263 D3). The
ring’s read buffer is offered to the kernel as capacity instead of being zeroed
first - a memset of the whole file the kernel then overwrote - and its length
grows by what each completion reports (file-ring’s README lists the one
set_len that takes). fs::read_to_string, read, write, map and the root
check write a path into an error’s text only when there is an error, where
every call used to. A program that never overlaps pays about 2,500-2,600
instructions a read for the runtime’s I/O, down from 3,100-3,500
(docs/history/runtime-cost.md §7.3). What is left is the hand-off itself,
which ADR-263 D1 is measured against next.
Two decisions, recorded.
ADR-262: the grammar parses the holes of
an f"…" (#107). A hole was found in the literal’s text after parsing and
parsed again on its own - which a walk written in Nikaia cannot do, so every
walk that must see a hole stayed Rust. f"…" becomes a rule of text and holes,
a hole an ordinary expression; the tree holds the parts; a hole is written as
code is written, f"{emphasised("Ada")}", and \" is refused there; a broken
hole is a parse error at its place; the emitter builds from the parts once it
measures the same. Takes ADR-032 §3’s option (a), deferred there for now.
ADR-263: a file operation on an idle
runtime runs on the calling thread (#106). Go and Java’s virtual threads do
the syscall on the calling thread; the pool runtimes hand off every time, which
is what Nikaia did. A regular-file operation with nothing else in flight - no
other task, no outstanding operation, no timer - is a blocking syscall where it
was asked, decided by std and not by the compiler or a setting, since what
overlaps is already written in the source. First the normal path stops doing
work it does not need, and the rule enters std only if it then measures.
Where a read’s 3,321 extra instructions go (docs/history/runtime-cost.md
§7.2): the ring ~1,030, allocation and copying ~830 - the read’s buffer is
zeroed before the kernel fills it - the executor ~450, and ~425 for the text of
an error fs::read_to_string writes on every call and reports on none.
What being async costs a program that never overlaps is measured (#106).
benches/awaits.nika reads a small file n times, one read after the other,
three .awaits deep, and measure.rs holds it against the blocking lowering
ADR-055 replaced and against the futures alone
(docs/history/runtime-cost.md §7). An .await costs nothing measurable - 3
to 25 instructions a read at user_parallelism = no. The read’s road through
the runtime’s I/O costs about 3,100 to 3,500 instructions a read more than a
read(2) on the caller’s thread, at both settings. Whether a read with nothing
else in flight should take the caller’s thread is left open, with its number.
The emitted code writes no unsafe for a view (#84, ADR-218 D4’s last
step). A value a task takes with it was packed beside a clone of the task’s
keep, its views stretched to 'static through tether::forever inside
unsafe { … } on the promise that nothing derived outlived the handle. It is a
Holding over the task’s keep now: built first, then carried in by Rebase,
which finds every view again in the keep by its address - so the promise is
checked rather than trusted. A buffer that may end in a task’s keep, or in a
keep a caller hands in, is put with put_viewed, so the views are found rather
than copied. tether::forever is gone; the emitted code writes unsafe only
around a call into foreign code (ADR-124 D3).
Rebase reaches every shape a task takes: a map, a set, a tuple, a Box, and
a number, a bool or owned text as they are. A task that took a
HashMap[ref String, i64] would otherwise be a file rustc refused.
What a call resolves to is answered in Nikaia (#125). tools/calls.nika
holds sync::reached, the one rule sync, throws and touches resolve a
call by (ADR-028): this package’s functions first, then the anonymous
constructor’s key, then the library by both names; a variant of a type a ledger
knows or this file declares builds a value and is no call; anything else is a
call nobody can name. The walk that hands it each expression stays Rust: it
reads the holes of a text literal, which only the parser can.
Moving it found three things the compiler did wrong, each fixed in the compiler (ADR-250 D3):
return written as an expression in a T? function handed back a
bare value, and the wrap it was owed landed on the statement around it:
let h = half(k) ?? return 0 was let h = Some(match … { None => return 0 }).
A plain value in a nullable slot is now wrapped by its own address, where it
stands.T? was not wrapped at all:
Callee::Opaque(name) with a name: String reached rustc as a String
where an Option<String> goes.Symbol’s clone was a call nothing described, so a function that
copied one out of a tree was inferred to pause. described.contracts
describes it.What an operation reaches is decided in Nikaia (#125).
tools/touch.nika holds the two decisions of contracts::touch:
stdout and stderr stay one destination under 2>&1, a
resource nobody could name might be any of its kind, and two reads never
conflict;touches its body earns -
the greatest one, taking the claim away from whatever reaches a function
without it, then the union over everything a function reaches.Reached is the Nikaia record now, and contracts::order compares two calls
with its methods. Reading a touches entry, the list of kinds and the walk
stay Rust, the walk with sync’s. Nothing a program is told changed; the move
compiled as written once the language’s own rule was followed -
self.named ?? … on a borrowed self is NK2106, and self.named?.clone()
is what it asks for.
A list with a null in it is a list of T? (#312). let xs: Vec[i64?] =
[null, null, 7] passed the checker and reached rustc as vec![None, None,
7]; the elements beside a null meet it as the arms of a choice do, and the
7 is Some(7) below. And x ?? -1 over an element a loop binds - a view of
an i64? - reads the copy, as a view of any copy value does: no Or below
took a &Option<i64>. A text literal beside a null where the list is
declared Vec[String?] is still a view where text of its own is wanted, as it
is in the arms of an if.
Which functions can open a lock is settled in Nikaia (#125).
tools/locks.nika holds the least fixpoint of contracts::locks, as
throws.nika holds throws’: a function that reaches a holder holds, a variant
is a value and opens no door, a callee no ledger describes is the doubt it is,
and a published function field stays undecided because packages that do not
exist yet store into it (ADR-230 D1). The walk stays Rust with sync’s.
Nothing a program is told changed.
Moving it found two things the compiler wrote wrong, each fixed in the compiler (ADR-250 D3):
?? chain that ends in a jump - a ?? b ?? continue - wrote the inner
fallback inside the outer one’s closure, so the continue stood in a closure:
rustc’s E0267. The whole chain is a match now, and the jump leaves the
function it was written in.for - for key in [a, b] - is walked as an array;
vec![..] there was an allocation clippy refuses in generated code.Writing the test found a third, filed as #312: a list of optional elements,
[null, 7] as Vec[i64?], writes 7 unwrapped.
Which errors leave a function is settled in Nikaia (#125).
tools/throws.nika holds the least fixpoint of contracts::throws - every
function starts with what it throws itself and grows by what its callees throw,
a callee the walk did not see still bringing the set the ledger gives it
(ADR-173 D3) - and what a throw names: io::IoError::NotFound is
io::IoError, ConfigError::BadSyntax { line } is ConfigError. The walk that
finds what each function throws and whom it calls stays Rust: it is sync’s
walk (ADR-028), and it moves with it. Nothing a program is told changed, and the
throws tests are unchanged. The move compiled as written - the first ring that
found nothing in the compiler.
The trait check is Nikaia (#125). What traits.rs checked - a method of
an impl Trait for T the trait does not declare, and one it declares that the
impl leaves out (NK1130), a receiver written differently from the
declaration’s (NK1196), and a method that can pause or fail where the trait
says it does not (NK1129, NK1140) - is tools/traits.nika, and
traits.rs is the adapter that hands it the tree and the ledger. What a check
of the toolchain reports is one record, Finding in tools/findings.nika:
a code, a span, a message, notes, a help and whether it is a warning - the
shape the next rings report in too. Rust reads it into its own Finding. The
messages are the same words, so the trait tests are unchanged.
Moving it found four things the compiler did wrong, each fixed in the compiler (ADR-250 D3):
match on a field reached through a view moved the field out of what
the function was only lent: match item.node with item lent was rustc’s
E0507. It is matched in place, and what an arm binds is a view of the field.item.node of a
Spanned[Item] was unknown, and so was every call on it.std’s ledger had no clone for its maps and sets.The ceiling of unanswered method calls in the corpus is 48, from 50: a tool is
read there with the Rust it reads described, as lower-std reads it.
A name bound to a map read is read as one (#297). let found =
calls.get(key) holds a view of what the map keeps, exactly as calls[key]
does, and it is now typed and refused as one: found ?? [] is NK1185 with
found?.clone() ?? … handed over - where it reached rustc as the trait
bound &Vec<String>: From<Vec<_>> is not satisfied - and found?.clone()
copies the list rather than the view. m.get(k) is a map read wherever it
stands, as m[k] was. And a map’s text beside a name that is a view of text,
imports.get(call) ?? call, is a view whichever side answers, written as it
is: it was an .into() rustc could not infer. The last line of
tools/surface.nika written around it is written the language’s way now
(ADR-250 D3).
The toolchain’s Nikaia is one package (ADR-261). Every .nika in
crates/nikaia-std/src/tools/ shares one namespace, as the files of a package
do, and nikaia lower-std checks and lowers them together into one file,
src/tools/package.rs, where seventeen .rs files stood. A tool names another
tool’s function or type with nothing in between: the BESIDE table is gone,
and with it the copies it forced - one slice, one find_text, one
split_top_level, one Refused where three files each declared the same one.
The names two files both declared are renamed once (rust.nika’s Item is
RustItem, manifest.nika’s Shape is DependencyShape, and five more). One
module per file at the end of package.rs re-exports what that file offers,
so a Rust caller still writes nikaia_std::tools::ty::Ty.
An error another file of a package declares is the package’s own. A
function that throws it travelled as Box<dyn Error> below, where one written
beside its impl Error travelled in its envelope - found by the package above,
and as true of a program of several files.
ADR-260’s status says what was built (0.0.298-0.0.300).
A guard reads what a box holds (ADR-246 D5 item 2, #97). The arm opens a part that holds its own type at its head, and a guard runs before that:
Expr::Add(a, b) if a == Expr::Num(1) compared a &Box<Expr> with an
Expr below - rustc about a file nobody wrote. Each boxed name the guard
reads is bound again, for the guard, to a view of the part, and compared as
one.Expr::Add(Expr::Num(n), b) if n == 0 was NK1193. The shape is asked by a
match through the box that binds n - copied out, as a number is - and
answers the guard where it fits.match was lent, Expr::Add(Expr::Num(n), b) => n
handed back a &i64 where the function said i64; a number taken out of the
box is copied out at the head of the arm, as one bound outside a box was.NK1193 is left for a pattern that looks inside a box inside a boxed part,
and for one that does so in one alternative of an |.
nikaia describe writes its entries and notes in Nikaia (ADR-104 D3,
ADR-193 D3-D5, #124). A function’s entry from its signature is
signature::contract; a type’s entry from its fields and derives is
crossing::type_entry; the notes on the crate’s promises and on a function
that reaches a thread are crossing::promise_lines and
crossing::thread_lines. describe.rs keeps the driving: the manifest, the
files, the grammar into the surface, and the file it writes. A draft says
what it said before, word for word.
nikaia describe reads what a crate offers in Nikaia (ADR-104 D2-D4,
ADR-193 D3-D5, #124). tools/surface.nika holds the crate’s table: every
fn by the path it is defined at, which modules are pub, what a pub use
carries out of a private one - a glob too - every pub type with its fields
and derives, and every unsafe impl. It reads the Rust grammar’s items and
the path work beside it. describe.rs keeps driving: the manifest, the
files, and turning the table into a draft. A draft says what it said before,
and a glob re-export has a test of its own now.
Which tool reads which module beside it is one table (sysroot.rs’s
BESIDE): a tool named there reads exactly its list, and every other tool
reads the tree, because a package is one namespace and the grammar’s Item is
not the tree’s.
Walking a set does not pause (#302). A for over a
collections::BTreeSet[String] or a HashSet bound a name of unknown type;
its .clone() resolved to nothing and read as a call that may pause, and the
function and every caller of it became async. A set’s element is its one
argument now, and every check on the binding sees it.
nikaia describe reads a Rust signature in Nikaia (ADR-104 D3-D4,
ADR-193 D4, #124). tools/signature.nika holds one Rust type in the ledger’s
words and whether a value of it is kept, what a Result throws, and which
parameters a Send bound reaches. It reads the ledger’s records for Ty. A
draft says what it said before; what is left in describe.rs is the crate’s
table of what it offers and the drafting around it.
A name a for lends over a list of text compares with text of its own
(#300). for one in words { if one == named { … } } for a named: String
compared a &String with a String below - rustc about a file nobody
wrote. The lent binding is read there now, as a lent number already was.
An Array written without its length is told the length (NK1166).
comptime WORDS: Array[ref String] = ["a", "b"] was answered with make it
an Array[ref String] - the type it already said. It says now that the
Array has no length, which a comptime needs, and hands over
Array[ref String, 2], the length the build computed.
A for over a T? is refused (NK1206, #296). for word in pick(1)
for a pick that hands back a Vec[String]? was checked as a walk over the
list and lowered as a walk over the option - once, with the whole list as
word - so word.len() counted the list and the program printed a number
nobody asked for. A map read is a T? too, so for x in m[k] was the same.
The help hands over ?? []; for a map, NK1185 then hands over the copy.
nikaia describe says what crosses a thread in Nikaia (ADR-193 D3-D5,
#124). tools/crossing.nika holds whether a type’s fields let it cross,
whether a bound names Send as a word, the scalars that travel unchanged, the
list of thread sinks, and the shortest way a function reaches one through the
crate’s own calls. It reads the ledger’s records beside it for Crosses, as
ledger.nika does. A draft says what it said before. std’s ledger gains
char::is_alphanumeric, which the Send word test needs.
A copy of a view is text of its own, written type or not (#293).
let last = current.trim() followed by out.push(last.clone()) read the copy
as the view itself, because only a local with String written on it counted
as text: current went into the frame’s keep, and a current = "" elsewhere
assigned a String where the keep’s place stood - rustc about a file nobody
wrote. A local bound from text - a literal, a trim, a copy - is text now,
whatever its let wrote.
A kept buffer assigned again goes into its keep too. Where a buffer
really is kept - a view of it outlives it - current = text.clone() now puts
the new value into the same keep, and the views taken before go on reading
the old one.
nikaia describe spells a crate’s names in Nikaia (ADR-195 D4, #124).
tools/paths.nika holds what was string work in describe.rs: the module a
file is, a path joined from its parts, what one pub use offers, where its
path could resolve, and a split on the commas outside brackets. Lowered to
paths.rs by nikaia lower-std; describe keeps the table the answers fill.
A draft says what it said before.
Asking a view whether it is empty no longer pauses. std’s ledger had
String::is_empty and no str::is_empty, so .is_empty() on a ref String
read as a call that could pause and made its function async - and every
caller of that function. It has its entry now, sync.
nikaia describe walks a crate in Nikaia (ADR-195 D4, ADR-196 D4, #124).
The Rust half no longer walks: tools/sources.nika reads every .rs under a
crate’s src with fs::walk and fs::read_to_string, is lowered by
nikaia lower-std to sources.rs beside it, and describe drives it to its
end. ADR-196’s sign of a stall - fs has a walk and the Rust half is still
doing the walking - no longer holds. What a draft says is unchanged.
fs gains a directory walk (ADR-195 D4, #124 step 1). fs::walk(path,
root) -> Vec[String] hands back every file under a directory, as names
relative to it with / between the parts, sorted. Directories are not listed
and each is visited once, so a link back up the tree ends the walk. Under
fs::Root::Dir the root holds for every name found, not only the one asked
for: a symlink that points out of it is left out rather than failing the walk.
It throws where the directory cannot be read and where a name is not UTF-8.
This is what nikaia describe needs to read a crate’s sources from .nika.
A pattern looks inside a part that holds its own type (ADR-246 D5 item 1,
#97). Expr::Add(Expr::Num(0), b) => simplify(b) was NK1193, with the advice
to bind the part and match on it in the arm; it is written as it reads now.
The lowering binds the part to a name, asks in the arm’s guard whether it has
the shape - so the next arm is tried where it does not, as for any pattern -
and takes it apart in the arm. Such an arm covers no variant on its own: a
match whose only Add arm looks inside its box is missing a case
(NK1151), said here rather than by rustc about the guard. Still refused by
name: a pattern that looks inside a box inside that part, one that does so in
one alternative of an |, and a guard that reads a name bound in there.
A build warns where a pub function lost the sync it had (ADR-244 D5,
#129), and with it ADR-244 is built (#92). Where the committed ledger says a
pub function could not pause and the build finds that it now can, the build
says so - NK2211, a warning - at the statement that made it, with the calls
that lead there:
warning[NK2211]: `parse` could not pause before, and now it can.
--> src/main.nika:4:5
4 | time::sleep(1.millis())
= note: `parse` calls `read_section`, which pauses here.
The author learns it at the change, rather than a consumer at an upgrade.
Committing the ledger the build wrote acknowledges it, and it is not said
again. The sync inference keeps, per function, the first statement that
pauses it and finds the shortest way to one through the package’s calls.
A build names what its pub functions could promise and do not (ADR-244
D2, #128). Since 0.0.303 another package counts only on a written sync, so
the author is told where one could be written: nikaia build, run, test
and lower end with a note listing the package’s own pub functions that
never pause and do not say sync, and those that pause only where the lambdas
they are given do and do not say sync(f), each with the word to write. The
sync inference keeps a call to a pausing lambda apart from every other pause
to find the second list; it still writes no promise itself. Only the entry
package is named, once per build and the same on a cache hit.
Another package’s function is sync where its source says so (ADR-244 D1
and D3, #127). A dependency’s sync = "inferred" is what its body happens to
be today, and a change inside it withdraws it; it was read as a promise, so a
log line in a library could break callers it never saw. A consumer’s check now
reads it as may pause: a sync function, a lock’s door, a par_iter lambda
or an action that calls such a function is refused, and told that the callee
doesn’t promise that it never pauses - with the word to write in the other
package - rather than that it pauses. The lowering still reads the body, so the
call is not awaited (D3); only a caller whose sole pausing call is an
unpromised one becomes may pause itself. Inside a package nothing changes.
Part III 13.5 says so.
Part III 14 says what assert is now (#104). 14.1 and 14.2 had been
rewritten to ADR-245’s function and ADR-256’s proof; what was left said that
nothing proves a claim and every row of --asserts reads run time, which
the report has not printed since the prover was built. It and --help now
name what the report says: proved, a precondition its callers prove, checked
by a test, or refused. And the explaining switches are written as switches
of nikaia build and nikaia lower rather than as nikaia --trust, which
stopped being a command line in 0.0.299.
nikaia run becomes the program it built (#281). It started the
program as a child and waited, so a nikaia run that was killed - by a
supervisor, by timeout, by this repository’s own HTTP test - left the
program running and holding its port. On Unix the process is now replaced by
the program, which has its id, its signals and its exit status; elsewhere it
is still waited on. The record of resolved dependency versions in
nikaia.lock is written before the program starts, since nothing of
nikaia runs after it.rustc ran, and Cargo dates a
dependency file to when the invocation started, so the source looked edited
mid-build. The driver, which lowers every member before Cargo runs anyway,
now writes it first.nikaia run f.nika compiles and runs one file (ADR-260 D4, #132). A file
outside any project is built as a project of its own, kept in the user’s cache
directory under a name made from the file’s path, through the same Cargo
workspace nikaia build makes; then the program starts. The interpreter is
not what run uses: it handles a part of the language, and a program means
one thing (Part I 1.2). The file is lowered where it is before anything is
copied, so a refusal names the file the user wrote and not the kept copy, and
the copy is rewritten only when the file changed. A file inside a project has
to be that project’s entry point, or is refused with the entry and both ways
out named. nikaia run without a file is unchanged; a file and --project
together are refused.
Found on the way, and filed: a new project is compiled again on its second
build although nothing changed (#280), and killing nikaia run leaves the
program it started running (#281).
The single-file commands are verbs (ADR-260 D3 and D5, #131).
nikaia --input f.nika named an argument and not what happened to it, and
three jobs shared its switches. They are commands of their own now, and each
one’s --help says in its first sentence what it does and what it does not:
| Was | Is |
|---|---|
nikaia --input f.nika |
nikaia lower f.nika: writes f.rs and the ledger beside it, and does not call rustc |
nikaia --input f.nika --backend interpreter |
nikaia interpret f.nika: runs the file, writes nothing |
nikaia --input f.nika --explain |
nikaia explain f.nika: reads rustc --error-format=json on stdin and reports it against the .nika |
--input is removed, not kept as an alias (D5), and so is --backend:
rust was the only code generator and a verb now says which job is asked.
Each is refused by name. --output belongs to lower alone. The explaining
switches and --locked, --no-cache, --target, --user-parallelism
stay where they were.nikaia with nothing to do says so, and names nikaia build and
nikaia lower.tests/samples/ (now nikaia interpret),
benches/ty-census, Part III 13.2 and the pages that describe the commands
moved in the same package. ADRs and measurement notes keep the command line
they were written with.A file inside a project is a file of that project (ADR-260 D1 and D2,
#130). nikaia --input on a project’s file read the [build] section of its
nikaia.toml and handed the lowering no packages, so use http in
examples/hello-http/src/main.nika was refused with add it to
[dependencies] about a line that was already there. It now reads the
dependencies the manifest declares, through the same packages_of that
nikaia build uses, and lowers the file to what nikaia build emits for it
(the abort table names the file as it was handed in). The cache key gains the
dependencies with them: their files are units of the program, so an edit to a
dependency is a miss. A file outside any project is standalone, and a use of
a package in it is told that a single file outside a project has no
dependencies, and that they are declared in a nikaia.toml.
And the lowering got faster again, measured on this compiler’s own use (#269, #273, #274, #277):
f"…"
and template hole again, each time with a fresh parse context and its own
intern cache: a fifth of lowering 1brc, as much as parsing the file.
Parsed::hole keeps the tree of each hole that parsed.std’s modules are
found from their few dozen distinct prefixes rather than from each of its
keys.winnow-grammar is pinned to a commit (27d2d3c) rather than following
its main, so a build is the build that was measured. Interned names hash
with ahash, keyed per process from the operating system (#275); keys
built into the binary would be keys anyone holding it can read, and a
parser that interns its input would be open to colliding names. Where a
WebAssembly target gets its keys is #276.Lowered with --no-cache |
0.0.297 | 0.0.298 |
|---|---|---|
1brc |
73.8 M | 54.2 M (−27 %) |
n-body |
72.6 M | 60.5 M (−17 %) |
k-nucleotide |
69.5 M | 55.6 M (−20 %) |
The ledger is read by grammars (ADR-257 §4.6), and what made them fast
was built in winnow-grammar, where every grammar gains it. The reader in
tools/ledger.nika walked the text a character at a time through Vec[char];
it is now grammar LedgerText for the lines, grammar LedgerValue for a list
and grammar SignatureText for a signature, slicing views out of the text
rather than copying it, and reads std’s ledger to the same records as before.
Reading std.contracts went from 11.4 M instructions to 5.9 M.
Measured on this compiler’s own use, winnow-grammar now
ParseError::from_input, used by the number literal and same_line);until(a | b) without allocating.Lowered with --no-cache |
0.0.296 | 0.0.297 |
|---|---|---|
| an empty program | 22.8 M | 17.4 M (−24 %) |
1brc |
102.8 M | 73.8 M (−28 %) |
n-body |
94.5 M | 72.6 M (−23 %) |
Parsing 1brc alone went from 21.1 M to 12.4 M (−41 %). Diagnostics are the
diagnosing pass’s, which tries every alternative as before.
x = rule(t) catch { throw Mine } made the function throw the guarded
call’s error as well as Mine; a handler with throw error still passes
it on (ADR-157 D2).for loop or a match arm binds is the view: an element of a
Vec[ref String] and a variant’s ref String part, reached through a lent
value, were a view of a view below, and w == "fn" did not compile.std’s (grammar::parse,
grammar::parse_pieces) rather than a block written in its place: behind
a catch the block was a match scrutinee with statements in it, which
clippy refuses.x? binds in a grammar may be absent: put into a nullable field it
is the field’s shape already, and the .into() it got converted an
Option into itself.[H: One, U] is two entries); a ledger’s entry lists the bounded ones, as
it did.std’s ledger is read once per run (ADR-257 §4.6). Most of what the
measurement called reading std.contracts was reading it again: seven
places parsed STD for themselves - the emitter twice, the checker’s
propagation twice, Program::emit_reading, Foreign::library and the cache
that was meant to be the only one. Every reader now borrows that one parse
(contracts::std_ledger), and one that adds a package’s own entries takes a
copy (std_library).
Lowered with --no-cache |
before | after |
|---|---|---|
| an empty program | 93.4 M | 22.8 M (−76 %) |
1brc |
186.8 M | 102.8 M (−45 %) |
n-body |
178.5 M | 94.5 M (−47 %) |
hello-http
building against http: it is read where it is absorbed (read_beside,
the ledger and its .derived) and by its own lowering, which compares it
with what it inferred (newly) - two readings with two purposes, 0.85 M
each.std’s ledger by reference rather than as a copy of its
own.clippy did not
have yet (collapsible_match, 1.98); fixed in the lowered ty.rs (#87),
and the toolchain here is the one CI runs.What the checker asks of a type is Nikaia (ADR-257 step (d)), which
finishes the record: fits, erase, parameterise, as_a_view,
is_seen, unseen, is_a_view and is_unknown are Ty’s methods in
tools/ty.nika, and bind, substitute, qualify and renamed its
functions. The toolchain is 8.4 % Nikaia.
TyOps keeps only the constructors from a name,
reading a type back and from_ast.fits is asked at every argument, assignment and
return: an empty program, 1brc, n-body and k-nucleotide each lower
within 0.2 % of the instructions the Rust took.ref Array[T] was ?, so a
method called on it was one no ledger describes, which may pause, and the
function around it became a future. It is the item now, and a range in the
brackets a run of it again (ADR-179 D1, ADR-215 D3).match arm that binds a boxed or copied part and is a
block opens with those names inside its own braces, where it wrapped a
second pair around them (unused_braces); and a bool literal after ??
carries no .into().match with one pattern beside
an empty else, which is how the language writes an if let.tests/reader_shapes.rs runs a method on an element of a ref Array[Row],
indexed and sliced.?? lends its left side at every position that only reads the answer
(ADR-259 §4, #98): an f"…" hole, a comparison of text and
the receiver of a method that only reads it, beside a call’s argument and the
print calls. A name used again after such a line needs no .clone() now,
where it was NK2105.
?? of text only: the language below compares a view
of text with text and with a view, and a view of anything else with a view
alone. Over a struct it takes, as before.lent_reaches asks for a ?. (ADR-189 D2).tests/nullable.rs runs the three with the name used again after each.A part an arm binds as a view is handed on as it is (#270). ADR-242 binds a match arm’s names ref where the arm only reads
them, and a call that lends such a name wrote &key all the same: a view of
a view, the same code below, and the lint nikaia-std allowed on its tools
for ledger.nika’s reader (0.0.292). The allowance is gone.
ref, in its f"…" holes as well.
Only for an arm that is a block and binds none of the names again.tests/reader_shapes.rs runs the shape: Line::Pair { ref key, ref value }
handed to a function that reads text, in a statement and in a hole.Reading a ledger back is Nikaia (ADR-257 step (c)), whole: the file’s
shape, what every table and key means, a signature back from the file’s
spelling, and each record’s text. Ledger::parse is one call into
tools/ledger.nika. The toolchain is 7.4 % Nikaia, from 5.5 %, and the
compiler reads std’s ledger in fewer instructions than the Rust did.
ledger.nika reads ty.nika’s records beside itself, as every tool reads
the tree: READS_THE_RECORDS in sysroot.rs names it, and lib.rs writes
the use super::ty::*. A list rather than every tool, because the records’
names (State, Shape) are words other tools use for their own things.touch_of, held_of, class_of, variant_of, signature_of and
unspell are Nikaia; the compiler’s TouchOps::parse and its siblings
call them. Gone from Rust: Ledger::parse’s body, spelling::unspell and
its helpers, and the value readers (sync_of, throws_of, borrows_of,
provenance_of, split_config, bounds_of).version that is not a number says so by line
(line 1: version is a whole number, not x) where it was Rust’s
invalid digit found in string.unspell answering a
signature that needs nothing turned back as it is.text.chars().collect() into a Vec[char] is
nikaia_std::list::chars, which asks for the room once. collect over
Chars asks for a quarter and grows; it was the largest single cost of the
reader, and every Nikaia module that lists a text’s characters
(Ty::parse among them) is cheaper for it.x == null on a type that does not compare was NK1188. It asks
whether there is a value and is lowered to x.is_none(), which compares
nothing.match in statement position wrote its arms as values: an arm
ending in m.insert(k, v) handed back the old value, and the arms
disagreed with an empty one. Its arms are statements now, as an if’s
in that position already were.async. The derived
copy of 0.0.290 covers the package’s types now.?. on a map read, m[k]?.clone(), wrote its scrutinee in
parentheses, which the language below warns about.?? whose fallback is a constructor the file declares
(FnContract::empty()) or a struct written out, or a variant of an enum
another unit declares, carried a useless .into().tools with the reason beside them. One of
them, a & before an arm’s binding that is bound ref already, was
#270 and is gone in 0.0.293.Ledger and Notes are Nikaia, which finishes ADR-257 step (b): every
record a ledger holds is declared in tools/ty.nika. The toolchain is 5.5 %
Nikaia.
Ledger is declared with ordered collections::BTreeMap and BTreeSet
tables and a blank() constructor; Notes with empty(). What the
compiler does with a ledger (infer, absorb, render, read it back) stays Rust
behind LedgerOps, which a caller imports beside Ledger. No ledger byte
changes.Default gave way where the language gives none: Ledger::default() is
Ledger::blank(), and project::Foreign writes its own Default.Signature, FnContract and TypeContract are Nikaia (ADR-257 step
(b)), and two defects the move found are fixed. The toolchain is 5.4 %
Nikaia.
tools/ty.nika declares the three records, with Signature::text,
takes_a_receiver and result_or_unit, and empty() for each where a
Default was. Reading a signature back and the slice of its arguments stay
Rust behind SignatureOps. Two fields are renamed because their words are
reserved: FnContract.throws is fails_with, FnContract.sync is
sync_claim; the ledger file’s keys do not change.clone of a type the program declares made the function around it
async. No ledger describes the derived copy, so r.clone() on a
declared struct was a method nothing describes, which may pause; the
function became a future, and main with it. It is the derived copy
now: it does not pause, cannot fail, and hands back the type, owned.
ADR-258 D3’s zero overhead, for every program that copies its own types.rows[1].a was
(*get(&rows, 1)).a, which the language below does on its own and
clippy refuses in std. It is get(&rows, 1).a, as a read of a read
already was (0.0.238).?? whose fallback is a declared variant built with its parts
(Ty::Tuple([])) is written without .into(), as a unit variant already
was.The ledger’s small records are Nikaia (ADR-257 step (b)), and a field on
the left of ?? is a place. The toolchain is 5.1 % Nikaia.
tools/ty.nika declares Provenance, Sync, Lock, Crosses,
Threads, Touch, State, Held, Count, Class, FieldContract,
VariantContract and ConfigContract, with the methods that say them
(as_str, text, join, or, holds, may). Reading their text back
stays Rust behind *Ops traits until the text form moves.Default: the named variant
(Lock::No, Count::Atomic, …) and hand-written Defaults on
FnContract, TypeContract and one of locks’ own records. Ord: three
places needed it, and each now says its key (touches by kind, parameter and
access; classes by members and Count::rank; join and or as a
match). The order is the derive’s, so no ledger byte changes. Whether a
declared type has an order stays ADR-204 §4’s open question.self.parameter ?? "" inside a ref self
method moved a field out of a borrowed value, and rustc refused the file.
A field on the left of ?? is a place now, as a name is (ADR-259): lent
where the answer is only read (self.parameter.as_deref()), and NK2106
where it is kept, with the copy as the way out.?? lends its left side where the answer is only read (ADR-259), the
owner’s choice of option (1).
?? gives when its left side is already a
view (two views join), and ADR-114 D4 and ADR-213 that a map read stays a
view. What happens to an owned name on the left had not been written down;
every one of those records points where this one goes.println(user ?? "guest") and shown(v ?? other) open the option
(.as_deref() for text, .as_ref() otherwise), copy nothing, and leave
the name usable. let name = user ?? "guest" and return user ?? "x"
take it, as let b = a does, and a use afterwards is NK2105. A fallback
is lent only where a view of it lives as long as the answer: a text
literal, a name, a declared variant. Part I 3.5 says so.f"…" hole,
a comparison and a method’s receiver still take, which is correct and costs
a .clone() where the name is reused: #98.Four weights for a design question (ADR-258), the owner’s philosophy.
open-decisions.md’s question on ?? is rewritten that way: a table of
what each option costs under the four weights. All four recommend (1): the
use decides, taken where the answer is kept and lent where it is only read,
as an argument is.A type’s text is read in Nikaia too (ADR-257 step (c)), and three defects the move found are fixed. The toolchain is 4.7 % Nikaia.
Ty::parse and split_args are tools/ty.nika’s, and the compiler’s
contracts::ty calls them. The Rust parser ran beside the Nikaia one first,
over every type the shipped ledgers write, the text of each read back, and
ninety-odd edge cases: 989 inputs, identical. The price is measured: this
language has no slice of text, so each piece is copied where the Rust
sliced; reading std.contracts costs 95.46 M instructions instead of
90.96 M (5.0 % of an empty program, 2.3 % of json). ADR-257 §5 records it.inner = args took args’s views into an inner declared around it, so
args went into the frame’s keep and the assignment put a view where a
String goes: rustc’s mismatched types. An assignment of the buffer
itself is a move now, as a return and a let already were.starts_with, ends_with or contains reached
rustc as Pattern is not implemented for String. It is NK1102
with write ref, and a literal, a character and a ref pass.?? and used again reached rustc as borrow of
moved value. It is NK2105 now. Whether ?? should lend instead is
open-decisions.md’s new question, with a recommendation..into() after a ?? whose fallback is a variant
of an enum the file declares; and manual_map joins the tool modules’
allowed lints, for a ?. on a method, which is a match because a method
may pause or fail (ADR-066). nikaia-std’s lib.rs says why.Ty and Shape are Nikaia (ADR-257 D1, step (b)’s first half), with
the text a type is written as. The toolchain is 4.1 % Nikaia.
nikaia-std/src/tools/ty.nika declares the checker’s type language: the
nine shapes of Ty, Shape with meets and missing, the words a
sequence carries, and Ty::text, which is what a diagnostic prints and a
ledger stores. The compiler’s contracts::ty re-exports them, and what was
impl Ty is the trait TyOps beside it until step (d). Display is five
lines in nikaia-std that call text. The field throws is can_throw,
because the word is reserved; Fn’s result is a Ty?, boxed where ADR-246
boxes it; and Shape::none() stands where a Default was. Re-lowering all
of std with the new compiler changes no file.NK2302). impl Shape { fn meets(ref self, wanted: ref Shape) -> bool
{ … self.missing(wanted) … } } was refused as keeping wanted in a
struct of three bools. The subject now answers as its fields already
did: a struct or enum whose parts hold no view keeps none.ref T?, reached rustc
as expected Option<&str>, found &Option<String>. name in
S::Named { name, .. } over a ref S is now a binding the language below
lent, and the call opens the option (.as_deref(), .as_ref()), as it
does where the compiler lends a T? itself.NK1137 named &: You don’t need the & here … Remove the &,
about a program that wrote ref. The parser refuses & (ADR-184 D4), so
the way out could not be taken (Part III C.2). It says ref now..to_string() on text is a copy. ADR-216 D4 (0.0.193,
the owner’s decision) made it the text itself, a view where the receiver
is one; the checker and its tests do that, and the sentence was left from
0.0.192. It says what the language does now.NK1167’s way out is one a program can write.
comptime declared String was told Declare it as &str, and one
declared as a Vec was told it can hold [T; N]. The parser refuses
&, which left the language at 0.0.134 (ADR-184 D4), and [T; N] is the
language below’s spelling. Part III C.2 says a way out that cannot be taken
is not one. The messages now say ref String and Array[T, N], and
tests/build_time.rs checks that neither spelling comes back.The checker’s types move to Nikaia as they are, and the measurement that decided it (ADR-257).
Ty and the ledger’s records whole, as the tree moved.
Before the measurement, ADR-257 D3 wrote down the rule for interning them
(a u32 handle into a pool, as Zig and rustc do), and D4 wrote down what
the measurement would show. Both were committed before it ran.Ty is 3.4–6.6 % of the front end on every program. The rule asks for 15 %.
The front end is 10.8 % of a build after an edit. Of the seven predictions,
P1 (the front end’s share of a build) was wrong, P2 held only for the
examples, and P4 half held; §4 of the record says which and why.Ty was
16 % of nikaia lower-std. It was two walks, not Ty.
Comparable walked every enum payload with its own copy of the cycle
guard.struct’s fields.Both are fixed and change no output. lower-std is 58 % fewer
instructions, and Ty falls to 4.6 %.
benches/ty-census/ has the scripts, so the measurement can run again on a
later compiler. It runs callgrind over the corpus, attributes cost to Ty,
runs a census build for the counts, and copies one program k times so size
changes and nothing else does. §4.7 lists what would reopen the answer.Ty (#122): reading std.contracts
is 35–89 % of every run, and the build’s rustc wrapper reads it again.
Resolving an interned name is a fifth of a large run.open-decisions.md loses its entry on the ring after the tree.A variant matched on a T? is refused (NK1205, #180).
match g(0) { W::B => 2, else => 3 } for a g that hands back a W?
passed the check and was lowered as a match over an Option<W> with
W::B as its pattern, which rustc refused. The pattern says nothing about
null; the help hands over ?? first and a match of what it gives. Part
III’s table of codes has the entry.contracts::trust is Nikaia (ADR-250 D1, D2), and three defects the move
found are this compiler’s now.
nikaia-std/src/tools/trust.nika holds the module’s decisions: which
written root is a way around the root check (Root::Anywhere,
Root::Dir("/")) and what --trust prints. The compiler’s
contracts::trust keeps the call walk and the ledger and hands over names,
bools and line numbers. The toolchain is 3.8 % Nikaia.null is Some: if b { 1 } else { null } and a
match with a null arm were a 1 beside a None below; the checker
records the plain arms of a choice that meets at T?, and the emitter wraps
them.String::push_str takes a view in std’s ledger, which said text: ?:
out.push_str(name) for a name: String is NK1102 with write ref,
where it was rustc’s expected &str.Array’s (ADR-184 D3): xs.len() for an
xs: ref Array[R] resolved to nothing, so its function was inferred to
pause.match over a T? with a variant’s pattern reaches
rustc (#180).The defects list is empty (#171 and #170): four found moving
fold into Nikaia, and two they led to.
match arm is a call of its function. The walk every
derived column reads (sync, throws, touches, keeps) passed by an arm
that is not a block, a list’s items, a with, a return written as an
expression and a select’s values: R::A => slow() left pick sync
while its lowering awaited slow(). throw error in a handler passes the
caught error on and adds nothing to the function’s errors, which the walk
now says, since it reaches that arm.match over a call binds has the variant’s type: match g(n) {
R::Value(c) => c, … } left c, and the whole match, untyped. A method on
it made its function look as if it paused (fold’s combined needed a
written sync, and needs none now), and a.name?.clone() moved the field.ref String?, is an Option<&str> (Part I 2.3): the
caller hands x.as_deref() (or x.as_ref() for other types) and None for
null, and b?.clone() in the body is to_owned().let a = 2000000000 then a + a - a is NK1116 at the step where a is
an i32, and accepted where a use makes it an i64.fold.nika writes return Folded::Nothing if op != UnaryOp::Neg and no
sync of its own.Three lowerings that reached rustc are this compiler’s now
(#161 and #171).
mut parameter given away whole - return out, Keep { items: out },
let taken = out - is NK1131, the shape ref self already has (Part I
6.6 says so now): it stays the caller’s, and the help names out.clone()
and taking it without mut. It was mismatched types below.op == Op::Neg for an op: ref Op is *op == Op::Neg. The answer is
keyed by the operator and the side, so a == b && Op::Add != a reads only
the second a.let wrote:
let l: u64 = match … { … => if n { 2147483648 } else { 1 } } is u64
throughout, where the if wrote 2147483648i64.fold is Nikaia, and a constant counts in a magnitude and a sign
(ADR-252 D6, decided by the owner).
nikaia-std/src/tools/fold.nika folds a constant integer expression in a
u64 magnitude and a sign - 65 bits, every literal ADR-248 D2 allows - with
no i128 anywhere in the language. The compiler’s fold is its adapter.let a: i32 = 2000000000
then a + a - a is NK1116 at a + a, where the lowering failed in rustc
before.match is copied out, and a lent function parameter handed on gets no
second & (a recursive one overflowed rustc’s trait solver). Five more are
written up as #171, one as §1.32.ast.nika beside it. The toolchain is 3.6 % Nikaia.A type a ledger says copies is copied (ADR-252 D4.1, the reading, for a described type).
contracts::keeps::lends_in and the checker’s takes_away read
copies = true off the library: a parameter of such a type is a value
below, not a &, and a value handed on is still there to hand on again
(NK2105 no longer fires on a second use).winnow_grammar::Symbol the way it passes a
number, which the first reader of the tree needs.copies for a unit’s own types.A tool reads the tree’s names as described (ADR-252 D4.3, the rest of it).
nikaia-std/src/tools/described.contracts describes the Rust a tool module
reads: winnow_grammar::Symbol copies and compares, and
InternerContext::resolve is sync. nikaia lower-std checks and lowers a
module of src/tools/ against it and std’s ledger; std’s own modules
still see std alone.PartialEq wherever its parts compare, and
OpaqueType is Copy.docs/open-decisions.md asks what a constant fold counts in once fold
is Nikaia, since the language has no i128.The syntax tree is declared in Nikaia (ADR-252 D1, D5 step 4).
nikaia-std/src/tools/ast.nika declares all of the compiler’s tree, and
nikaia lower-std writes ast.rs beside it. crate::ast re-exports it, so
no reader names a new path; what stays Rust is what Nikaia cannot say — a
Span’s usize doors and the backend’s WithSpan in nikaia-std, and
int_value over an i128 in the compiler.Expr::Return and Type.code are Box<Option<…>>,
Code.result is boxed too, and Pattern::Fold is not. Its twenty readers
follow the declaration (ADR-252 D3).Span::nowhere() replaces the derived Default, for D4.4’s reason.scripts/self_hosting.py), from 2.4 %.A described Rust type says whether it copies and compares (ADR-252 D4.3).
nikaia describe reads a type’s #[derive(…)], by each path’s last segment,
and drafts copies = true for Copy and compares = true for PartialEq.
A type with no derive says neither, which is no (ADR-010 D1).Checked::copies), as it is for a number; == on it already read
compares.winnow_grammar’s Symbol waits for step 4, which is the first to read it.The prover reads lengths and sees into lambdas (ADR-256 D5).
xs.len() of a list, text or array binding that does not change is a
variable of the proof: never negative, known for a literal, and carried into
a callee’s precondition, so fn first(xs: Vec[i64]) can ask
assert(xs.len() > 0) of every caller, and return fallback if xs.len() == 0
proves it.A parse error names the mistake where the reader made it (Part III C.2,
rule 5). A parse stops where the input stopped making sense, often a line after
the mistake; a failed parse is now read once more for the mistake itself. All
twenty-four failing rows of docs/error-corpus.md say what a reader needs,
where 0.0.266 left twelve that misled.
, is missing before temp., There are two commas here., = has
nothing after it., if needs a condition before its {., let names
what it binds, but 5 is a value., = gives a name a value; a condition
compares with ==., This { / [ / string is never closed. at the
bracket or quote that opened it, and the rest in the corpus.par_fold( and fold( commit to a fold, so a wrong count is said at the
call; an arrow inside a pattern points at the commit =>, where deleting it
would silently have made a plain sequence.a, b and c is said only inside a lambda; ...args is
no longer a sketch to the specification’s walk.docs/error-corpus.md describes today’s messages and how they are found.Every assert outside a test is proved while the program is built
(ADR-256), or the program is refused.
A check at run time was a cost nobody wrote down and an abort nobody planned
for; now there is none.
crates/nikaia/src/prove.rs: linear whole-number claims, proved by
Fourier-Motzkin with integer tightening from the branch an if is in, what a
jump with its condition after it rules out (return 0 if b == 0, ADR-255),
a for over a range, a let’s value and the claims before. Sound, and
incomplete: what it cannot prove is NK1202.NK1203). Not yet on a pub fn, a
method or a function handed on as a value (NK1204).assert is still the test’s verdict. --asserts says for each
claim how it is held. Part III 14.2, ADR-245’s header and C.3 say so.What the specification’s examples found in the compiler, fixed.
NK2606 is no longer raised for a failure caught inside the lambda.NK1191 covers arithmetic on a declared struct or enum, and xs += [3];
NK1201: a list, a map or a set in an f"…" hole.fs::Root::Dir("site") lowers its literal to a String; a DSL statement’s
parameters handed to something that is no driver are NK1109, and the
dsl X from e help says X::rule(e).nowhere::wobble(1)) is
refused here or left to rustc is on open-decisions.md: the fix found for
it contradicted Part III C.4 and was taken back.A jump may carry its condition after it (ADR-255).
return 250 if speed > 250, throw TooFast(speed) if speed > 250,
continue if n % 2 != 0, break if done: each is if cond { jump }, and the
parser builds exactly that if, so nothing after it changed.
return, throw, break and continue, and only on the jump’s
own line; return if c { a } else { b } is still a return of an if.tests/jump_guards.rs runs all four.A checked statement runs on a connection it names as its subject, and the
connection’s type carries its schema (ADR-254).
The owner answered open-decisions.md’s database question with option 1, and
added the schema: a statement checked against one schema handed a connection
to another is refused while the program is built.
by_age.execute(db; min_age: 18); a driver’s open takes the schema.
Part II 10.5’s example and its NK1112/NK1113 transcript, Part III 17.1’s
sqlite example, ADR-143’s header and #89 say so. Not
built.open-decisions.md instead: how a schema is bound at file level, what
Path is, whether fs::exists takes a root, which Shared Part I 6.2
means, how a package’s DSL is named, html::Raw::new in Part III’s
listing, and the image example’s comment on locking.A list has no + (ADR-253). The
owner answered open-decisions.md’s question: no operator on a list, and
NK1191 stays.
NK1191’s note no longer calls the question open: Lists have no
operators: + doesn’t join two lists. Its help still names a.extend(b).extend; the entry
leaves open-decisions.md.Every message in plain words, in one layout (Part III C.2, rule 5). Good messages are a claim this language makes, so the rest of the codes follow the two 0.0.265 began with.
count is changed, and a let
that is changed says mut became You’re changing count, but it wasn’t
declared as mutable. No message cites the specification or a decision
record any more; those stay in the documentation. The same for the
refusals of the lowering, the manifest, the module loader, nikaia
describe, and the notes about threads, locks and build-time values.rustc message, NK2202. The
headline is a sentence; --> names the place; the source line is shown
with the place underlined, the named word where the message knows one and
the statement’s first line where it does not; = note: and = help:
follow. The renderer capitalises and closes each sentence, once.}
here, but found temp. - and no longer list the rules the parser was
inside (in coalesce_tail, in expr, …). A message the grammar writes itself
is split into a headline and a help.NK1152’s help printed {callee} unfilled; an assignment to
a SharedMut was reported twice, as NK2204 and as a missing mut;
NK2605 quoted the ledger’s list syntax (["io::IoError"]).A message says what you wrote, why it cannot work and what to write, at the
places it is about (Part III C.2, rule 5). Feedback on the website: its
example message read like rustc’s.
^ under the place the error is, - under a place that explains it.
It had shown one ^ under a statement’s first byte. Finding::labels
carries them; a finding without labels renders as before.NK1138 and NK1139 are the first two in the new voice: You’re
changing count, but it wasn’t declared as mutable., with the change and
the declaration both shown, and Add mut where it’s declared: let mut
count. The report now stands where the change is, which is where the
reader’s editor should go.Copy reached a type with a Drop: Part I’s FileHandle { fd:
i32 } writes impl Drop, and rustc refuses Copy beside one (E0184). A
type with a Drop or a cleanup (ADR-239 D1) derives no Copy now; the
specification’s corpus test found it.throws field is can_throw, a word Nikaia lets a field have;
and two compiler defects found by lowering a draft of tools/ast.nika are
fixed - a ring of types through a declared generic struct was not boxed, and
a generic struct was asked whether it compares about its parameter rather
than what it was given.A declared type whose every part is a copy derives Copy
(ADR-252 D4.1, first half): a unit-only
enum, a struct of two u32, and anything made only of numbers, bool,
char and such types. It is what the syntax tree’s Rust readers need of
BinaryOp and Span once Nikaia declares them.
tests/copies.rs has rustc accept every
derive written.nikaia_std::tools allows clippy::clone_on_copy until then, with the
reason: template.nika writes at.clone(), as the language requires.An integer literal is its magnitude and its sign, and the tree derives no
Default (ADR-252 D5 step 2), still in
Rust: two things Nikaia has no word for, taken out of the tree before it moves.
Expr::LitInt { value: u64, negative: bool } where it was an i128, which
no Nikaia number holds. It is what the source writes and exactly ADR-248
D2’s range; ast::int_value gives the number to the eight readers that
compute with it.FnParams::none() and FrameAttr::bare() are named where a derived
Default stood.A Span is two u32 (ADR-252 D5
step 1), still declared in Rust: the largest diff on the road to moving the
tree, and entirely mechanical, so taken alone.
ast::Span is Span { start: u32, end: u32 }, a copy, where it was a
Range<usize>; bytes(), at() and stop() are what the Rust readers
ask it for. The at offsets of four expressions are u32 with it.parser::fits),
saying why, rather than wrapping every offset past the end of a u32..clone()s of a span are gone, and every spanned node is 8 bytes
smaller.The syntax tree moves to Nikaia whole, and a Span is two u32
(ADR-252), the record ADR-250 D2 asked
for before the tree moves. The leaves are gone - fixed, emit::template and
manifest are Nikaia - and every module left reads the tree, which no Nikaia
module can read while Rust declares it. The owner chose the tree moving whole
and u32 offsets.
tools/ast.nika will declare all 35 types, and crate::ast
re-exports them; the Rust that stays reads the Nikaia declaration, and
nothing converts.Span { start: u32, end: u32 }, a copy; a source over 4 GiB is
refused; the at offsets are u32.Spanned[T] is a Nikaia generic struct, and ADR-246 D1 places the
boxes.Copy
for a declared type, no number that holds LitInt’s i128 range, no way
for a Rust crate’s type (winnow_grammar::Symbol) to copy or compare, and
no Default. Each is its own package before the move; the literal becomes a
magnitude and a sign rather than the language gaining i128.Span in Rust first, then LitInt, then the language,
then the move.What nikaia.toml may say is decided in Nikaia
(ADR-250 §5). tools/manifest.nika
decides which [build] keys exist, have moved or are withdrawn, which machines
and keys [build.<target>] may carry, and which of the three shapes a
dependency is; Manifest::parse reads the file with the toml crate, which
stays behind the adapter (ADR-250 §3), and asks it about every key. The
toolchain is 2.5 % Nikaia.
x = { type = 1, path = "../x" } was read
as a dependency with no type, and passed as a Nikaia package with its
marker silently ignored. A type that is not a word names no ecosystem, and
is refused like type = "c".manifest needs a TOML grammar; §3’s correction
(0.0.253) keeps the crate, and D2 now says so too.A read’s answer reaped by another thread wakes its reader
(ADR-121 §5). The arm64 CI job has hung
six hours on every run since at least 0.0.252, in
fs::many_threads_reading_and_writing_at_once_each_get_their_own_answer, and
the job cancelled by the next push hid it.
file_ring::Ring::answered counts operations’ completions
reaped; rt::with_ring moves the generation when a call reaped any, while
it still holds the ring.rt::tests::an_answer_another_thread_reaps_moves_the_generation reaps one
thread’s read on another and fails without the fix. The hang itself did not
reproduce here on x86 (16 runs of the binary, one core and four); the test
holds the invariant the park depends on.changes has
timeout-minutes: 90. On main the runs queue in one group, so a job that
hung held every later push’s answer for GitHub’s six hours, and the run
waiting behind it was cancelled by the next push - which is how this one
stayed hidden.The ledger’s reader is Nikaia, the half that knows the file’s shape
(ADR-250 §5). tools/ledger.nika reads
which lines of a nikaia.contracts say something, which open a table, where a
key ends, what a quoted string holds and where a list’s entry ends;
Ledger::parse keeps what each key means. The toolchain is 2.3 % Nikaia.
pub enum has a variant with two payloads,
"Pair(i64, i64)", or whose pub field is a HashMap[String, i64], wrote a
ledger its consumer refused (expected a quoted string, found "Pair(i64).
The Nikaia reader splits outside quotes.text.lines() over a lent parameter kept it, because
Ledger::candidates counted io::lines() - a function, no self - as a
method lines might be. Only an entry that takes self is a candidate.mut was kept for being changed; it is a
&mut by the author’s word (ADR-094 D3), like ref mut self at 0.0.257.mut parameter handed away whole, Box { items: out },
reaches rustc (#161).ledger_spelling.rs (a list entry with a comma reads back whole; the
reader’s list directly), keeps.rs (a method name a free function shares; a
mut parameter).keeps no longer names a receiver the author lent (#173,
found at 0.0.254 in the ledger tools/template.nika lowers to; ADR-251 §5).
Position::escapable(ref self), whose body is one match self, was recorded
keeps = ["self"]; so was Splitter::until(ref mut self, …), for calling
itself; and Scan::feed(ref mut self, c: char) kept its char.
ref self or ref mut self is not kept. It is a
reference by the author’s word; a use that would need it whole is NK1131’s.
An argument written ref keeps as before - a parse keeps the text its result
views (ADR-186 D1), and filtering it out broke that at 0.0.254.match over an argument written ref does not keep it. The
over-approximation that arm makes is for a parameter the lowering could
still lend, and a ref one is lent already.c.to_ascii_lowercase() leaves the caller its char.State::Quoted(c) keeps the char it is given.
The column says what a body does (tests/keeps.rs holds that).template.nika’s ledger names no kept parameter; no committed ledger and no
lowered std file changes.CI is green again on Rust 1.98. Stable moved under the repository
(ADR-001 D1 names the channel, not a version), and 1.98’s clippy brought
explicit_counter_loop, which failed main at 0.0.252 through 0.0.255 on the
Rust the emitter wrote for template.nika’s here: a for over a word’s
characters beside a let mut counting into the scan.
(start..).zip(…), and a
for that walks one sequence while a counter moves is how the language says
it. nikaia_std::tools allows that one lint, with the reason beside it;
every other lint still holds the lowered code..rs of a moved module is
read by a newer clippy than the one it was lowered under, so a toolchain
release can fail a package that did not change. The local toolchain is now
the one CI installs, and a package is checked with it before it goes to
main.sync(f) is in the language (ADR-244
D4). A function that runs a lambda promises that it pauses only where the lambda
does: fn apply(f: fn(i64) -> i64, x: i64) -> i64 sync(f). std has written
the promise by hand for map, filter and the lock doors since ADR-029 D3; a
program can now write it.
f is the one pause allowed, and any
other is NK2202, apply is sync(f), and time::sleep can pause.NK2210): a parameter that takes a function
and runs it during the call - not a name that is no parameter, not a number,
not a lambda the function keeps.sync where its lambda is: twice(x) sync may call
apply(fn(n) { n * 2 }, x), where without the word it was NK2202. A lambda
that pauses inside a sync caller is refused there, in the caller’s words.async fn its body is (D3); a caller that cannot pause
drives the call once with nikaia_std::rt::exec::settled, which the checker
has proved is enough, and one that can awaits it.sync = "sync(f)" from the declaration (ADR-251 D4).NK2210.sync(f), and ADR-244 D1, D2 and D5 (what a
consumer reads across a package boundary, and the notes that go with it).The ledger speaks Nikaia where Nikaia can say it (ADR-251 D4, D5).
(ref self),
(ref mut self, c: char), and self: ref T where its type is not the entry’s
own. mutates = true beside the signature is gone.ref(a | b) in the result:
-> ref(self) String, -> Seq[ref(self) T]. returns stays only where the
result has no ref to carry it, and is spelled returns = "ref(self)".[T](x: T) -> T,
[K, V](mut self: ref collections::HashMap[K, V], key: ?) -> Entry[V], where
$T stood.sync = "sync(f)", the word ADR-244 D4 gives
the source, where "from(f)" stood.contracts::spelling, writes and reads it; the reader still takes
the spelling before it. std.contracts and the hyper_shim descriptions
moved line by line, comments kept, and each reads to exactly the contract it
was.keeps, touches, locks,
crosses, threads, provenance, compares, iterates, sharing, Seq,
Par, Seen, ? - and Part III 13.5 says so.keeps names a ref receiver a body only reads (#173
#173), fail-closed and not yet precise.The ledger is the contract, and what it was derived from is beside it (ADR-251 D1-D3). Asked before the ledger’s reader moves to Nikaia: the format was two files in one, carried prose the compiler never read, and Part III 13.5 described a file no longer written.
nikaia.derived holds toolchain, inference and [sources], beside
nikaia.contracts; a description has contracts/<crate>.derived beside it.
Both are generated, committed and compared under --locked; a contract is
believed while its sources hash as its record says (ADR-100 D3, unchanged),
and one with no record beside it is not. A compiler upgrade or an edited
comment no longer changes a published contract. The build cache keeps the
record as a third artifact. Format version 3.doc in the contract (supersedes ADR-139 D2): a ledger with one is
refused. std.contracts keeps its 249 sentences as comments above their
entries.sync keeps its three states (D3). The contract’s spelling moves to Nikaia’s
next (D4, D5), and sync(f) into the language after that (ADR-244 D4, decided
and not yet built).keeps, mutates, variants,
compares, iterates, ends_by_length, threads, provenance, and
locks = "?".crates/nikaia-std/src/tools/nikaia.contracts, a hand lowering’s leftover
committed at 0.0.169 and read by nothing, is removed and ignored.The road to Stage 1, and the fifth module on it
(ADR-250). ADR-001 D4 named Stage 1 -
Nikaia compiling Nikaia - and this says how it is walked: a module at a time,
leaves of the module graph first, the Rust module left as the adapter and the
whole suite as the proof. The compiler is 2.0 % Nikaia (876 of 44,718 lines
that say something), measured by scripts/self_hosting.py and drawn into a
README badge.
emit::template is Nikaia: nikaia-std/src/tools/template.nika, the
HTML scan that decides where a template’s hole sits and whether escaping can
make it safe (ADR-017). Position and Segment are Nikaia enums the emitter
matches on; the Rust module is an eighteen-line adapter. The scan walks
characters - the Rust it replaced kept its last nine bytes and aborted on a
character outside ASCII.f(self.field) into a parameter that only borrows was NK1131;match binds over a lent value had no type, so why.clone() in
an Error’s message made it a future the generated Display could not
print;String moved whole into a struct literal or an enum variant was taken
for a view of itself and put in a keep (ADR-209), where it became a
reference;.clone() of a trimmed text was not seen as text of its own.std.contracts: char::is_whitespace and char::to_ascii_lowercase.A number without an annotation is typed by its uses, and this compiler decides
it (ADR-249). Part I 2.4’s the use
decides had been the language below’s inference; ADR-248’s u64 and u32
gave a number a second answer, and an index gave rustc none.
let of numbers is open (D1) until the function has been
read: the parameter it is handed to, a typed value it meets in an operation
or a comparison, an annotation, a field, a return and an assignment each
ask for a type (D2); an index asks for an i64 only where nothing else does.
let b = 3000000000 handed to a u64 or put in a u32 compiles, where it
was NK1102 and NK1103.NK1200: two uses that ask for two types, naming both, with the
annotation to write.let mut n = 1 then
n = 3000000000 is an i64. A value the decided type does not hold is
NK1116 with a note naming the use - let a: i32 = 1, let b = 3000000000,
a + b is refused at b, once, where it was NK1199.let (D4): let i: i64 = 0;.
v[i].push(x) over let i = 0 runs (#177, closed), and the
lowered std sources carry their counters’ types.NK1116 wherever it stands (D5):
in an f"…" hole, a condition, a list, a receiver - once, at the innermost
operation - where rustc said this arithmetic operation will overflow
(#176, closed).NK1116 says a u8 and an i32 as the names are said.u64, u32 and the bit operators (ADR-248),
decided because fixed.rs asked for them - and fixed.rs is Nikaia, the
fourth piece of the compiler moved by ADR-196’s route.
u64 and u32, with every rule i64, i32 and
u8 have - an overflow aborts, wrapping_, saturating_ and truncating_
name the other answers, as converts and is checked where it may not fit.
No two number types mix on their own: u64 + i64 is NK1199, and so is
i32 + i64, which reached rustc before.u64 (D2): 0xcbf29ce484222325 is a u64’s;
with only i64 beside it, NK1116 names u64. An unannotated let whose
constant an i32 does not hold is an i64 to the checker too, so
let b = 3000000000 beside an i32 is refused in this language’s words.& | ^ << >>, ! on an integer, and &= |= ^= <<= >>= (D3-D5), in
Rust’s order. A bit operator beside a comparison is NK1197, the
parenthesised line handed over; on a bool it is NK1198, naming &&.text.bytes() (D6): the UTF-8, one u8 at a time.tools/fixed.nika: FNV-1a and CHD, lowered to tools/fixed.rs; the
compiler’s fixed module calls it.v[i].push(x)) reaches the element as a place rather than through a read;
for x in v[i] iterates the element; x == null is x.is_none(); a borrow
of a lent read is not written twice; a literal above i64::MAX carries a
u64 suffix._ in a for binding: for (_, bucket) in order, for _ in 0..<n.i32 sum that overflows inside an f"…" hole
(#176), and an untyped index into an element a call changes
(#177).Three things the lowering wrote that it should not have, each found moving Rust into Nikaia at 0.0.248.
list(item, sep) is lowered (Part II 10.8, #165): the
backend has no such element and read list as a rule of the grammar’s own,
so the macro said expected ident about the generated file. Each grammar
that writes one is given two template rules it lowers to - the first item,
then every sep item after it, or nothing - with the element type taken
from the item rule where the grammar declares it. tools/http1.nika reads
its request line with list(WORD, " ") now.match that only answers true or false is a matches!: the arms
that give one answer, and else the other. The long form is what clippy
refuses in std, and tools/dsl.nika had asked a text of characters
instead of writing a range; it writes the range again.name: name in a struct literal is written name, for the same reason.dsl.nika asks whether a name begins (a letter or _) rather than
whether a character is a digit, which is the question and which clippy
reads without a complaint.Two more pieces of Rust are Nikaia, by ADR-196’s route: written as .nika
under nikaia-std/src/tools/, lowered ahead of time by nikaia lower-std,
committed beside it and include!d.
dsl body (tools/dsl.nika): the compiler’s scan for
:name deferred parameters. dsl::parameters in the compiler is the call,
and tests/self_hosting.rs holds it to the Rust it replaced.tools/http1.nika, ADR-038 D6, ADR-194 D5):
a grammar that reads the request line and the lines with a colon, and read,
which says what a server answers and what it refuses - GET and POST,
HTTP/1.x, a Content-Length that is a count, no Transfer-Encoding - with
every refusal in the words it had. std::http1 keeps the bytes: the buffer,
where a head ends, whether it is text. The roadmap’s parsing moved into
Nikaia step is done.use std::M in a module of std’s Nikaia half is the import itself:
such a module is lowered without the prelude, so use std::text wrote only a
comment and text::parse_i64 below it named nothing.list(item, sep) (Part II 10.8) reaches a
backend that has no such element (#165); http1.nika writes
the repetition out.The lock column resolves a call the way sync does, and 11 of its 12
doubts in examples/ were only that. The analysis NK2201 and NK2203
are to read (ADR-039 D3) answered
undecided for 12 of 61 functions in examples/, and the roadmap still said
24 of 59. Traced call by call, the doubts were:
Stats(n) is the anonymous
constructor, recorded as Stats::new; HashMap() is
collections::HashMap::new, String() is String::new. The lock column
looked the name up as written, found nothing and answered undecided.
sync::reached already tried the constructor’s key “by the same rule for
both analyses”; the lock column did not use it. It does now.Json::Array(items) builds a value and runs
no body. It is no now, as it is for sync — except a last segment new,
which is a constructor nothing describes and stays the doubt it is.std has and its ledger did not write. html::Raw::new
exists in crates/nikaia-std/src/html.rs and had no entry; it has one
(sync, touches nothing).What is left is one function, http::answer, which calls its handler
parameter: whether that takes a lock is the caller’s lambda’s answer, which is
a real doubt and not a lookup.
The ledger entry found a program that broke a rule unseen.
examples/escaping wrote html::Raw::new(built); with std describing the
constructor, ADR-140 D2’s NK1149 applies
and the example writes html::Raw(built), which lowers to the same call.
crates/nikaia/tests/lock_column.rs: an own type’s constructor, std’s
constructor and a variant are each no; the test fails on 0.0.246.Patterns, lookups and a map’s lists: found by writing a graph and a few
shapes and running them, and kept in
crates/nikaia/tests/defects_found_running.rs.
_ is the ignore pattern inside a pattern (ADR-145 D2): (0, _) and
Shape::Circle(_) were refused with the catch-all arm’s message, which
itself says _ stands in a tuple position. A part of a pattern is now its
own rule; the arm is still else. Part I 2.1 and 3.4 say where _ stands,
and 2.1 no longer lists the arm, which ADR-145 D1 moved to else.AsKey an untyped 4 got the language below’s default integer, and
s.contains(4) on a HashSet[i64] asked for an i32. A plain & lets the
collection’s key type say what the number is; a key of no known type that
may be a view still goes through AsKey.Vec::contains is in std’s ledger, lent as a set’s lookup is.m[k]?.clone() copies the value, not the reference: a map’s read is a
view already, and lending it again made each element a view of a view.m[k] ?? [] is NK1185, for a map’s value that is not text: the read
is a view of what the map holds and the fallback is one of its own, and the
help names m[k]?.clone() ?? …. rustc said the trait bound &Vec<i64>:
From<Vec<_>> is not satisfied.Loops, keys and lists of functions: six programs found by writing small
ones and running them, each kept in crates/nikaia/tests/defects_found_running.rs
and run at both settings of user_parallelism.
for binding is a binding of its own. for (k, v) in seen, after an
outer k had been written into the map as a key, read the loop’s k as the
one handed over and refused a correct program with NK2105. A read and a
hand-over are now compared only where they name the same binding, which
every binding carries a number for; a read of the outer k after the loop
is still refused.for over several names binds each part’s type: a map’s key and
value, and the parts of a list’s pairs. Both were unknown, so nothing about
either was checked.bool or a char a for binds out of a place is the
element. The body opens with let n = nikaia_std::num::value(n);, so the
name is the value in every position below, where ADR-182 had told only a cast
and an annotated let: a field took a &i64 and rustc refused it.for lends is a view, typed as one, so keeping it without a copy
is NK1106 (or its neighbour) with write .clone() instead of rustc’s
expected String, found &String.m[k.clone()] =
(m[k.clone()] ?? 0) + 1: the read’s lent form was the write’s too. The
checker’s record of a key says which of the two it is about.Vec::pop is in std’s ledger. Without it every function that popped
was taken to pause and lowered async, and a generic type’s own pop
calling its list’s was read as calling itself.Four programs that reached rustc as a file nobody wrote, each found by
running something and each kept, as the program that found it, in
crates/nikaia/tests/defects_found_running.rs, run at both settings of
user_parallelism.
sync function calls a parameter whose type says sync
(#166). The caller is held to the word (NK2206), so the
body may take it at it; it was refused with NK2202. A parameter without
the word still is.Shared written into a variant takes the variant’s count (#158). The
part was declared with one count and the value built with another, and
rustc said expected Shared[E], found Shared[E]. A variant’s part now
has a place of its own in the sharing count, as a field has.?. chain through two nullable fields of a lent value (#167) lends
the first step when the member it reaches comes out as a view, as it did
when the member copies.NK1117,
with the near miss where there is one (did you mean doubled?). It
lowered as written and rustc said cannot find function. A name brought
in by a use item is declared.wide, serve, load_users, fetch_config, load) are refused
by NK1117 now instead of by rustc; EXPECTED.txt and COMPILES.txt
are regenerated. A test in tests/modules.rs that compiled one file of a
package through --input without its manifest gains one.A program whose result is a file is tested like one that prints — ADR-247, decided after comparing how other languages and test tools do it: no language has syntax for it, and the tools agree on a directory per test, trees in and out, blessed expectations and differences.
NAME.in/ and NAME.out/ (D2): what goes in is copied into that
directory first; each file NAME.out/ names must be there afterwards with
exactly its bytes. A test is a NAME.stdout, a NAME.out/ or both.nikaia test --bless (D3) writes the expectations from what the program
did - standard output and the files NAME.out/ names - for a run that ended
well and, at --both-settings, agreed. A new test starts empty and is
blessed; the review is the diff in version control.- expected and + produced,
with two lines of context.fs::scratch() (D5): a fresh, empty directory as an fs::Root, for a
test block to write under. Under nikaia test it lives in the test’s own
directory and goes with it. test blocks run in the package’s directory.tally/, report/ and inventory/ are packages now, each testing the file
it writes; inventory/ also has a test block that writes under
fs::scratch(). tests/examples.rs holds no expected output of its own any
more: all eleven runnable examples test themselves. The examples whose inputs
lay loose in tests/ keep them in NAME.in/. Three generated files committed
under examples/inventory/ by an old --input run are gone.std gains str::contains and str::ends_with, found by the first
assert about a page: an assert may only call what the ledger says
changes nothing.std::fs section and the table of NK codes
(NK1190’s wider reach, NK1194-NK1196) are written.The examples test themselves — ADR-245 D8’s first users.
calc/, access-log/,
config/, json/, n-body/, k-nucleotide/, escaping/ and trend/,
each with nikaia.toml, src/main.nika and tests/, where NAME.stdout
is what the program must print and NAME.args, NAME.stdin and the input
files beside them are what it is given. calc/ also carries three test
blocks: precedence, left folding, and a refused operand. nikaia run and
nikaia test work in each directory.tests/examples.rs runs nikaia test --both-settings in a copy of each
package instead of holding their outputs in Rust; its table keeps the three
examples whose result is a file (tally, report, inventory/)..args names input
files as the package’s own tree has them (tests/access.log).examples/ one level deep read a package’s one
file too (common::standalone_programs), so the move took nothing out of
them; every link to the old paths points at the new ones.Output tests, and --asserts — ADR-245 D8 and D6, built.
tests/NAME.stdout in a package makes NAME a test of its program, with
NAME.stdin as its input and NAME.args as its arguments, one a line.
nikaia test builds the program as nikaia build does, runs it, and the
test passes where it ends successfully and prints exactly the file; a
failing one shows what was expected and what was printed. They run beside
the package’s test blocks, and at both settings under --both-settings.
Each build is kept where the next one does not overwrite it: the test build
and the program land at the same path.nikaia --asserts, beside --overlaps, --sharing and --tethers:
every assert by line, and whether it was proved, refuted or left to run
time. Every row says run time, because nothing proves a claim yet.examples/ into packages, so their expected outputs can be output tests.nikaia test — ADR-245 D1 and D7, built.
test "name" { … } stands where a fn may, in any file of a package, and
sees what the package sees. test is a reserved word. nikaia build and
nikaia run leave every test out, unchecked; nikaia test compiles them.main with one that runs the test whose
number it is given; nikaia test then runs the program once per test. A
failing test does not stop the others. A test fails on an error that leaves
it (printed in its long form), a false assert or a panic; the command
prints a line per test, what each failing one said and a summary, and exits
1 where any failed.--both-settings builds at user_parallelism = no and yes and fails a
test whose outcome differs between them (D7).rustc
message about a file nobody wrote:
throws function ending in panic(…) or return had an Ok(())
after it, and rustc warned unreachable expression;T::new(…) - now NK1190, which hands over the literal;impl method taking self where the trait declares ref self (for a
trait of the file, or Error, Drop and Cleanup) - now NK1196.test.assert is a claim — ADR-245 D2 and D3, built.
assert(cond) and assert(cond; message: "…"): a prelude function the
compiler knows, so it needs no use and costs no reserved word; a program
that declares its own fn assert calls its own. A false claim stops the
program, as panic does, and says the .nika line, the message, the claim
as written and the value of each operand of its comparison:
src/math.nika:2: the program stopped: assertion failed: division by zero
assert(b != 0; message: "division by zero")
b is 0
An operand whose type has no printed form is left out rather than the
program refused (nikaia_std::abort::Operand).
NK1194: the condition changes nothing. Every call in it must be
sync, throw nothing, touch nothing and change nothing, and one nothing
describes is not known to; a block, a jump, a ? or a task in it is refused
the same way. That is what keeps a later proof possible (D3), and why reading
an operand again for the message is the same value.NK1195: an assert with other than one bool and a text message:.--with-asserts are gone.nikaia --input /abs/one.nika then
nikaia --input one.nika served the first lowering to the second, and its
aborts named a path nobody wrote. The names the output writes are in the key
now (tests/cache.rs).The first piece of the compiler is written in Nikaia - ADR-196’s route, used to take Rust out of the compiler.
std/tools/spelling.nika: the edit distance behind the checker’s did you
mean (optimal string alignment, so a swap of two neighbours is one edit)
and dsl’s narrower one edit apart. Lowered ahead of time by
nikaia lower-std to spelling.rs, committed, include!d as
nikaia_std::tools::spelling and called as ordinary Rust; the two Rust
functions it replaces are deleted. tests/self_hosting.rs holds the answers.rustc as a file nobody wrote:
d[i][j] = v) - the base of
an index write is now written as a place all the way down;for over a list lent to the function (long: ref Vec[char]) bound a
name of unknown type, so everything about it went unchecked; it binds
the element now;char or bool compared with a value
(short[at] == c, c == 'x' where c is lent) is read, the way ADR-233
D4 already reads one for arithmetic. Two views still compare as they are.d[i][j]) is get(get(&d, i), j) and no longer
get(&(*get(&d, i)), j), a borrow of a deref that clippy refuses in a
std written in Nikaia.nikaia-std names itself nikaia_std (extern crate self), so a module
lowered into it resolves the paths the emitter writes.std reads a whole number from text — atoi, written in Nikaia.
text::parse_i64(s) -> i64? in std/text.nika, beside digit_value: an
optional - or + and decimal digits, and null where the text writes no
i64 - empty, a stray character, a space, or one past either end of the
range. It counts downward so the smallest i64 is a number and not an
overflow. null and not an error, because is this a number has one answer
to a caller that only asks that; a text whose shape has to be explained is a
grammar’s, with dec[T](p).tests/self_hosting.rs): a
grammar, a syntax tree that holds itself, a check for names nothing declares,
and Rust written out as text, with none of the three workarounds it needed
at 0.0.231. A change that stops it building is a step back on the road to a
compiler written in Nikaia.std::text.A type may hold itself — ADR-246, the owner’s answer to how a type holds itself: the compiler puts the box in.
enum Expr { Add(Expr, Expr) }, struct Node { next: Node? } and a ring
through two types are programs as written. A field whose inline type reaches
back to the type that declares it is Box<T> below; it is built in its box
(a literal, a with, a variant’s constructor), read as (*base.field) - a
place of its declared type, through ?. too - and a name a match binds out
of it is opened at the head of the arm by std’s new boxed::open, which
makes a T of a box taken and a &T of a box lent. NK1192’s refusal is
withdrawn; the walk that found such types finds the fields to box.NK1193: a pattern that looks inside a boxed part
(Expr::Add(Expr::Num(n), b)) is refused by name with the way that works -
bind it and match in the arm - until the lowering rewrites it
(#97).Stmt::Say("a")) is built into text of its own, and
a variant’s constructor now types and hands over its arguments; and a number
bound out of a match over a lent value is copied out, so
Expr::Num(n) => n over a ref Expr is the i64 it says.Expr
directly, where it needed a list of two children.?. chain through two nullable fields of a lent value takes the
first one out (#167).tests/recursive_types.rs, at both settings of user_parallelism.A map keyed by numbers can be read — closing #169.
m.get(k), m.contains_key(k) and a
set’s contains(k) and remove(k) passed a number key by value where the
language below takes a reference, and rustc said mismatched types. A
method’s arguments are not lent by the ledger’s column (the declaration would
be written off it), so the checker lends a std lookup’s key itself, for
exactly those entries: &k where the key’s type is known, and
index::AsKey::as_key(&k) where it is not (let k = 1 has no type until
something pins it) - which lends a number and passes a view through. A
String key looked up is no longer handed over, so it is still there after
(NK2105 said get keeps it before).collections::HashMap() with no annotation, then m[1] = 2 and m[k],
went through index::at, which made the map keyed by usize and the read a
mismatch. A write now hands the key over as it is and a read goes through
index::key, the same number-or-view answer; a map of views reads as before.tests/map_reads.rs.Two more shapes refused here rather than by rustc, and the question each
leaves on open-decisions.md. Both found on the way to writing a compiler in
Nikaia.
NK1191: arithmetic on a list, a map or a set. [1] + [2] lowered as
written and rustc said cannot add Vec<i64> to Vec<i64>. Part I 4.5
gives a collection no operator; for two lists the help names a.extend(b),
which the ledger now describes (Vec::extend, the list keeps what it is
given). Whether + should join two lists is on open-decisions.md.w[0] +
w[1] over a Vec[String] moved the element out and rustc said cannot
move out of a shared reference; each side of a text + that is an index
read is now lent as a view.NK1192: a type that holds itself inline. enum Expr { Add(Expr, Expr)
}, struct Node { next: Node? } and a ring through other types or tuples
reached rustc’s recursive type has infinite size. The help names a list;
a view, a function, a list or anything the checker cannot see through is
taken to be an indirection, so no correct program is refused on a guess.
How a type should hold itself is the owner’s question — it is what
stands between Nikaia and a parser written in Nikaia — and it is on
open-decisions.md with a recommendation: the compiler puts the box in.Shared field and the value put in it disagree on the count
(#158). #172 is closed.tests/list_operators.rs, tests/recursive_types.rs.std::process: another program, started and waited for — ADR-243, the
subprocess ADR-195 D4 decided and nothing
built, and the thing a compiler written in Nikaia needs to run rustc.
process::run(program, args; dir: …) runs a program to the end and hands
back a process::Output: code, ok, stdout, stderr. A non-zero exit
is an answer, not a failure; io::IoError is a program that cannot be
started (NotFound(program), PermissionDenied) or output that is not
UTF-8. args is Vec[String], so [] has a type and a list of literals is
built into owned text. No touches (a child may touch anything), and the
output is untrusted.rt::io::beside), with a worker’s bells and pending count - so it holds up
neither the program’s other tasks nor an I/O worker. Proven by a program:
two children in an overlap at user_parallelism = no, one waiting for a
file the other makes.rustc (#172).len in std.tests/process.rs, at both settings of user_parallelism.Two defects found writing a miniature compiler in Nikaia, to measure how far self-hosting is.
collections::HashSet() reached rustc. The ledger described HashSet,
BTreeMap and BTreeSet as types with no constructor, so the call lowered
as written and the language below said expected function, found type
alias. Each now has its ::new entry and the methods a program first asks
for (insert, contains, len, get, contains_key, remove), and
HashMap gains contains_key. NK1190 refuses a std type called as
its constructor where the ledger describes none, so the next such gap is
refused here.let mut s = "a" then s = s + "b" was refused, NK1105 with a help to
take a view of the sum. A let mut of a literal that the body grows with
+, += or an f-string now holds text of its own, as
let mut s: String = "a" does (ADR-207 D2). A literal assigned, or a bare
name, keeps the binding a view — best = w over a list of views is the
ordinary shape — and a bare name of owned text assigned to it gets a help
naming the annotation.m.get(k)), and m[k] on a map
whose type was inferred lowers as a list position. #169.tests/anonymous_constructors.rs, tests/handed_over.rs.An impl of a trait nothing declares is refused here, not by rustc.
impl db::Connection for Fake lowered as written and the language below said
unresolved import db about a file nobody wrote; impl Connection for
Fake said cannot find trait. Nothing looked at the trait an impl names:
a bound fails open on a trait it does not know, which is right for a bound,
but an impl writes the name into the program. Found while designing
ADR-143’s database protocol.NK1181. A bare name is NK1117,
nothing declares a trait called Connection, and it is declared by
Error, Drop and Cleanup, a trait in any ledger the program reads, a
use that brings it in, or a foreign name. impl Struct for X is refused
with a note that a shape bound is answered by the declaration (ADR-088 D2).tests/traits.rs, both refusals and the correct programs beside them.Four defects found while checking the examples — closing #160, #154, #155 and #163 #160–1.20.
par_fold is refused. A fold’s step lambda
had untyped parameters, so acc.record(m) resolved to nothing and a pausing
record compiled into a future the fold dropped: the parse printed {}. The
accumulator is now typed from init (a type’s name is its constructor) and
the item from the folded rule, so the call meets NK2209 like any action’s.
A merge or init written as a function’s name is asked the same question.?? is NK1125. A catch
expression typed as nothing, so everything bound from one did too:
let report = Log::file(data) catch { … return } left report.paths[path]
a lookup on an unknown map, and counts.hits reached rustc. The catch is
now the guarded value’s type where its handler leaves, hands back that type,
or hands back something the checker cannot name (Part I 7.1 says so in a
sentence). Where a lookup’s value type is unknown, the message says this
value may be absent instead of naming the operator ??.
catch surfaced a false NK2105 in examples/http: two loops
that each bind more and hand it to take were one name to the check for
a read after a hand-over. A let now gives a name a value again, as an
assignment does.lent_arguments.rs used a bare catch as its value of unknown type; it
uses an if whose branches the checker does not unify instead.examples/foreign-runtime/serve builds and runs. A text literal
handed to a Rust crate’s generic parameter (across_a_thread<T: Describe>,
Describe for String only) lowered as &str. A kept, by-value $T that
only this argument decides now takes the literal’s own type, String. The
checker already said so; the emitter’s own walk read std’s ledger alone and
never resolved the callee, so it now reads the described crates’ ledger too.
A test with a dependency-free crate covers it on every run, since the hyper
test fetches from crates.io and is ignored.cfg trace warning on a program with a grammar. The
grammar! expansion tests feature = "trace" in the crate that invokes it,
so the generated manifest now declares trace = ["winnow-grammar/trace"]
wherever it depends on winnow-grammar, as nikaia and nikaia-std do.A match over a place lends what an arm only reads —
ADR-242, closing #168.
match over a name the function owns types each arm’s bindings from the
variant, and binds a part that does not copy as ref where the arm only
reads it. The name is whole after the match: matched again, printed, or
handed on — where rustc said use of moved value, and in a handler
borrow of partially moved value: error.push, drain), assigned, returned, or the
arm’s value. Numbers are copied as before.tools/rust.nika’s lowering changed by one ref, and the sysroot test’s
probe pauses through time::sleep instead of through the old gap.A library’s error travels in an envelope too — ADR-241, closing #199.
benches/envelope puts one word on the Result and no time
on either path (success ×0.95–×0.98, failure ×0.98–×1.03 of the bare
channel, inside the control’s spread).Thrown<E> everywhere, not only where
a body joins, and a sum’s member for one is Thrown<E> with a From<E> for
a ? straight from std. A caller that only propagates a joined failure was
refused by rustc (no From<Thrown<IoError>> for IoError); it now has its
callee’s channel and the list goes with it. A grammar’s entry rule still
hands its parse error bare.throw of a library error has a site; a catch opens the envelope, so
match error names the library’s variants.error.full() in a handler now includes the failures that joined the error,
for a program’s own types too.match that
binds a field of a name moves the name.A sequence’s count() is an i64 — closing #164.
count() kept the language below’s usize, so a count returned as an
i64, bound to one or added to a length was rustc’s mismatched types.
The checker now records each count() that resolved to std’s count of a
sequence (Seq::count, Par::count), and the emitter writes the conversion
a length gets, parenthesised where an operator or a method stands on it. A
program’s own count is left as it is.crates/nikaia/target/floor/, 2,114 build files committed by
accident with 0.0.200, and ignored crates/*/target so a build run from
inside a crate’s directory cannot be committed again.An error keeps its site when it hops into the box — ADR-240, closing what #199 #199 named as the box’s remaining cost.
Thrown<E> is no longer an Error; std writes
From<Thrown<E>> for Box<dyn Error>, which makes a Raised with the same
site, trace and list. A ? from a typed channel into the box now keeps
them: error.full() names the callee’s throw instead of no site
recorded, and what joins the error later is kept.raised at both), from
settle_after, branch_after and the new settle_at.An overlap branch settles its own cleanups and joins its own error —
ADR-239 D3, closing the cleanup half of
#199 (ADR-115 D3).
nikaia_std::cleanup::branch and branch_after: an overlap branch is
polled with a queue of its own, so what one branch parks is not another’s
to settle. Where the branch’s channel is the box it is settled at the
branch’s end, and a cleanup that fails while the branch is failing joins
that branch’s error; otherwise what it parked goes to the settle point
around the overlap. A block branch counts as fallible where a value it
binds has a cleanup that can fail.Joined for Box<dyn Error> puts an envelope on an error that reached the box
without one — a library’s, or a typed channel’s after a ? — so what joins
it is kept rather than dropped; full() says no site recorded for it, as
for a library’s error.Ownership says when a value dies, and the compiler settles its cleanup there — ADR-239, refining and building ADR-006, closing #188 and the call half of #199’s cleanup attachment.
impl Cleanup for T is the one method fn cleanup(ref mut self), throws
where it can fail; the synchronous fallback is the type’s own impl Drop.
Anything else in it, a drop included, is NK2601, pointed at impl Drop.nikaia_std::cleanup: a value with a cleanup is held in Cleaned<T>
(CleanedSend<T> at yes), whose Drop parks the cleanup in the running
task. The compiler writes settle points after a block where one is bound and
around a function body where one may die — a let, a literal, a call, a kept
parameter, a field or element that holds one. Values are cleaned up in the
order they die; a pausing lambda’s body and a task’s body settle as blocks.x.close() runs it at the call and hands its failure
back there.NK2605
(this function can fail because the cleanup of f can fail), NK2202,
NK2209 and NK2206 answer it with the value named. NK2602 is retired.main runs in order under
cleanup-deadline; one the deadline cuts off is named, exit status 70, on
the panic path. A panic parks nothing.fs::create(path, root) hands back a buffered fs::Writer (write,
flush), whose cleanup writes what it holds.main was done, the drain never reached its park, so
a task nobody joined that slept afterwards waited out the whole deadline;
and NK2106 named a field twice (self.path.path) where the argument of a
std call was walked twice.A word-sized value that crosses is the word, and update is a
compare-and-swap — ADR-238, closing
#184 (ADR-110’s speed half).
nikaia_std::lock::Word: an AtomicU64 whose update runs the block on a
copy and compare-and-swaps it in, again on a collision; set_after is one
compare-and-swap; a panic in the block leaves the word as it was.word: a SharedMut of a number,
bool or char whose crossing it proved (a spawn, a parallel walk), that
no access_all/update_all holds and that the floor did not answer for.
It reaches every position of the value, so one value is one Rust type. The
cheap single-thread shape is never replaced, and no never gets it.--sharing prints word with the reason, says why a word-sized crossing
value kept its lock, and names a get that copies a text or a list out of
its lock (access reads it in place).A cleanup point that moves is narrated, and a described call that fails compiles — ADR-236 and ADR-237, closing #186 and #194.
NK2403 (warning): a parameter whose keeps changed since the committed
ledger, on a type with impl Drop or impl Cleanup (directly or through a
field), is named at each call that hands it a name — where that name’s
cleanup runs now. Once; committing the ledger acknowledges it.rustc met a &Handle where the list wanted a Handle.throws and the emitter, which read std’s ledger alone, did
not write the ?. It reads the descriptions now, for that question only.? on a
non-Result, a Result where none was expected — is reported as the
description of X does not match the crate or the ledger of X does not
match its sources, with the backend’s words kept as a note (ADR-100 D6).check::NewlyThrowing is check::Newly, carrying throws and keeps.par_iter() walks a list on every core —
ADR-235, closing #185
(ADR-105 step 4).
Vec::par_iter is a Par[ref T] with Seq’s whole surface, and
Seq::for_each is new. At user_parallelism = yes it is
nikaia_std::par, a wrapper over rayon that keeps the list’s order; at no
it is the list’s iter(). The same program prints the same at both.NK2209; one that changes a name bound
outside it — sum += x, seen.push(x) — is NK2107, with map and
collect or a SharedMut as the way out. Both asked at both settings, each
with a tally line of its own.SharedMut a parallel lambda changes through update keeps its atomic
count: the sharing analysis reads a walk chained onto par_iter(), or onto a
name bound to one, as a crossing.join over a plain walk — xs.iter().map fn … then .join(",") — was
rustc’s error; nikaia_std::seq::Join is its counterpart.A lazy walk of io::lines() carries its failure to the walk —
ADR-234, closing #182.
io::lines().map fn(l) { … } was refused by the lowering, write the loop.
io::Lines is now a Step whose item is the line or the failure, and
map, filter, take, skip, step_by and zip on it — with a plain
lambda or one that pauses — hand back another such sequence
(nikaia_std::seq::Failing).collect, count, nth and join hand back the first one, and a
for handles the lines before it and then leaves with it. A failure is never
a shorter list.throws, so the
function walking it says so, as for the lines themselves.x86_64-linux wall names
aarch64-linux too, which 0.0.218 made a target.A lambda that pauses is handed to std’s pausing counterpart —
ADR-233, closing #187.
xs.iter().map(fn(x) { fetch(x) }) was refused by the lowering, not
something this compiler can build yet. map and filter with a lambda that
pauses now hand back a sequence whose step pauses, walked as io::lines()
is: a for gives its thread up at each step, collect, count, nth and
join pause with it, and map, filter, take, skip, step_by and zip
chain onto it. Below, nikaia_std::seq::Paused over a Step trait — the
trait ADR-172 D3 deferred until a second producer needed one.map, sort_by_key, or_insert_with and and_modify await the
lambda where they would have called it (sort_by_key works each key out
once). A lambda that does not pause is lowered exactly as before.NK2202, which Part II 12.2 said and nothing
asked: access, update and access_all bodies reached the lowering’s
refusal with the wrong reason. Its own tally line, a pause with a lock held.map resolves to its ledger entry in the checker, so its lambda is
typed, and a read of the list after it is NK2105 rather than rustc’s
borrow of moved value.ref i64 goes where an i64 is wanted (the * is written), arithmetic
over one is a number, and an element of words.iter() goes to a ref String
parameter or to a String one the callee only reads — each was NK1102.io::lines()’s lazy walks, whose step can also
fail.The machine named is the machine built for: aarch64-linux is a target,
the default is the machine the compiler runs on, and the triple reaches Cargo
as --target (ADR-037 D1).
Target::triple() appeared only in a refusal
message, and Cargo was run without --target. Every build was for the host,
so on an ARM machine the default x86_64-linux built ARM code under an x86
name - the one thing D1 says a target never does - and a build could not be
for any other machine than the one it ran on.--target <triple> for every project build, at the host
too, so there is one path. The built program’s path was already read from
Cargo’s own messages, so the <triple>/ directory Cargo adds moves nothing.aarch64-linux (aarch64-unknown-linux-gnu) has threads and the runtime,
and unwinds on a panic, as x86_64-linux does; [build.aarch64-linux] is a
codegen table a manifest may carry.x86_64-linux everywhere: a build that names nothing needs no toolchain but
its own. std’s committed Rust is lowered at a named x86_64-linux, so it
does not depend on where it was lowered.std for is refused before Cargo runs, with
the rustup target add that installs it and a word about the linker a
foreign machine needs; Cargo’s own answer names core.nikaia build --target aarch64-linux on x86_64 builds an aarch64
binary that prints the program’s line under QEMU, and the refusal appears
with the target removed. Two tests: the triple’s directory appears under
CARGO_TARGET_DIR, and aarch64-linux parses with the host as default.The whole suite runs on 64-bit ARM in CI: a new arm64 job on
ubuntu-24.04-arm.
asm!, no target_arch, and
the triple the target switch names (x86_64-unknown-linux-gnu) is only
ever printed, never handed to rustc - so a build on an aarch64 machine is a
native aarch64 build. Measured under QEMU before adding the job: the
workspace builds for aarch64-unknown-linux-gnu unchanged, the interpreter
prints the same as on x86_64 for every sample, nikaia-std passes 132 of
132, the orchestrator 30 of 30, and every unsafe crate passes its tests.unsafe crates. It runs on
every push to main, weekly and by hand; on a pull request only where
nikaia-std, an unsafe crate or a dependency changed
(scripts/ci-changes.sh) - atomics, memory ordering, io_uring and mmap are
what differ between the machines.target switch still only knows x86_64-linux, which on an
ARM machine names the wrong one while building the right one. io_uring is
not exercised under QEMU (it answers ENOSYS, and the runtime falls back),
so the job on real ARM hardware is the first to run it there.A published parameter takes either kind of text from its own package —
ADR-232, the README’s row about handing a
view to a pub function.
pub fn a view and text of its own for the same
String parameter was refused, naming .clone(). The ledger still says
String, so another package hands over text of its own as before; below,
the parameter is impl IntoEither<Either = EitherText> and the package’s
view is borrowed. Nothing copied, and no caller elsewhere can tell.clippy alone (a doc comment read
as a list, a redundant slice), which hid the test step behind it. Fixed; the
Rust-floor job had run the whole suite green on those commits meanwhile.The target-info probe test runs in every sweep again, closing #201.
the_target_info_probe_ignores_what_is_on_standard_input was #[ignore]d
because it flaked inside a fully parallel -p nikaia run, and #201 also
recorded Cargo’s package-cache lock showing up in two project tests’ output.
Measured again: the test passed in two full parallel -p nikaia sweeps, in
the first run after a rebuild, and three times beside one_name; the lock
message did not appear. The entry described mechanisms that no longer
reproduce, and a test that covers ADR-166 D1 end to end is worth having in
the gate.A value read through a view is the value —
ADR-231, closing #220’s
filter defect and the class it belonged to.
"a-b".chars().filter(fn(c) { c != '-' }), for x in xs.iter() { if x > 2
… }, xs.iter().map(fn(x) { x > 2 }) and h.keys().filter(fn(k) { k != "b"
}) all failed in rustc: a view is a reference below, and a number read
through one does not compare with a number..copied() below); filter’s lambda takes an item that copies by pattern;
keys() and values() say they hand back views.A function field is reached through what is stored in it — ADR-230, closing #175 (ADR-039 D7’s stored lambda).
NK2203
asks a call through it, directly or through a chain of calls. Where the
column cannot read what is stored (an assignment, a with, a published
field), the field is undecided, never quiet..await
into a synchronous closure (rustc’s error); it is now the refusal a pausing
function gets there, naming the field.rustc’s use of moved value); it takes its own
handle now, as handing a hull on does.A value handed over inside an f"…" hole is handed over there —
ADR-229, closing #200.
collect
written inside a hole now goes into a position both kinds flow into as it
does anywhere else. The hole used to keep that position text of its own,
which refused the lines outside it that put a view into the same list or map.A ?. view out of a temporary is held for the rest of the block —
ADR-228, closing #192.
let a = find(1)?.label() ?? "none", where label hands back a view of
the User, failed in rustc with temporary value dropped while
borrowed. find(1) is now bound on the line before and the reach reads the
binding: nothing copied, nothing run that did not run.??, &&
or ||, a match arm, after another call in the same statement, a block’s
value - the program is refused, naming the let it can write.find(1)?.name, ?.home?.x) were
already right, and are unchanged.collect() builds what the place it goes into declares —
ADR-227, closing #208’s
collect() into a declared map, with the owner’s choice over a refusal.
String
where a let, a field or a result declares one the items fit. The emitter
used to write .collect::<Vec<_>>() into a map, which rustc refused.either_keys(), either_values() or either_pairs()
(nikaia_std::either_text::EitherPairs).map makes as what its lambda hands back, and no
longer counts a number, a truth value or null as text of its own.filter lambda that
compares its item fails in rustc (&char against char).A view handed back out of a parameter that holds views is the buffer’s — ADR-226, closing ADR-222 §3’s last row.
fn first(xs: Vec[String]) -> String { return xs[0] } over a list of views,
a view field of a struct handed back, and a field both kinds flow into handed
back all failed in rustc with missing lifetime specifier. The first two
name the buffer’s lifetime on the parameter and the result (fn first<'a>(xs:
&Vec<&'a str>) -> &'a str); the third takes its struct, as a String field
does, because EitherText cannot leave a loan without an allocation.struct Seen overflowed the compiler’s stack: the
lock’s stamp Seen[T] was erased even with no argument, to itself. The stamp
now needs its argument, and a bare Seen is the program’s type.A view cut from text is typed as one —
ADR-225, closing #260’s a
view pushed into a list a published function handed back reaches rustc.
lines, split, split_whitespace, trim_start and trim_end are in
std.contracts, handing back views of the text they are called on, and a
String receiver finds str’s methods. They were ? to the checker, which
fits anything, so no question about a view cut from text was ever asked.Vec::push
says value: $T, and a view pushed into a Vec[String] that has to stay
text of its own - a list a published function hands back - is NK1102 with
ADR-208’s explanation and .clone() named, where it was rustc’s error.into_either()) reads as text of its own, and
a view handed to a parameter both kinds flow into is let through, as into a
mixed field.repeat to
to_ascii_uppercase, since String now finds str::repeat.The rest of the text wall — ADR-224, #200’s remainder.
String? is a position: views only make it a view that may be absent,
both kinds EitherText that may be absent, and null counts as neither. A
view put into a String? field, result or let passed the checker and failed
in rustc; where the position stays text of its own it is now refused with
ADR-208’s explanation.nikaia_std::either_text::IntoEither (and IntoEitherMaybe into a
String?), written value.into_either() by the tier pass and either(value)
by the emitter, with the nullable wrap around it. It replaces .into() and
EitherText::from.f"…" hole hands each kind to a keeping parameter as it
is, and a mixed parameter the callee only reads is a &str that every caller
lends to. ADR-223’s hole exception is left only for a push written in one.collect through
either_items(), and a list that already has a position — a result, a field,
a declared name — is linked to where it goes, one representation with
nothing converted. let c = words(text) then c.push(f"tail") passed the
checker and failed in rustc; now words hands back text of either kind.String is NK1102
naming .clone(), and to its ref String crosses free — already the
checker’s behaviour, now recorded and tested.collect() into a declared map, and a view pushed into a
list a published function handed back, pass the checker and fail in rustc.Every declared String is a position —
ADR-223, the owner’s Nikaia does not build
walls, it takes them down.
ADR-222’s three tiers now cover a function’s parameter, an annotated let, and
the element of a list or a set and the key and value of a map, each decided by
what the package puts there. A view handed to a function that keeps it, let s:
String = line.trim(), a list of the trimmed lines of a file, and a map counting
its words all compile and run with nothing copied. The last three used to pass
the checker and fail in rustc. A mixed position gets each value as
value.into(); the checker reads it as text of its own, so it is lent and
copied as a String is. A published parameter is never both kinds. A value
reaching a mixed position only through a string’s hole leaves that position
text of its own (its line cannot be rewritten), which keeps ADR-208’s refusal
for it.
What the first green-by-design run of 0.0.204 found, on a runner with fewer cores than the machine it was written on.
file-ring).
drive - what a synchronous read or write runs to its end - submitted
every unfinished slot, including a future’s that already had a submission
with the kernel. A write’s bytes reached the file twice
(many_threads_reading_and_writing_at_once_each_get_their_own_answer read
"2:72\n2:72\n"), and a read would have had the kernel write the same bytes
twice at once, which the crate’s own soundness comment rules out. A slot with
a submission in flight is never submitted again, and drive runs only its
caller’s slots; a_write_in_flight_is_not_submitted_again fails on the old
code and passes on the new.a_worker_operation_wakes_the_park_on_either_path passed only beside other
tests. park_for answers false both for nothing to wait for (at once)
and for the bound ran out (after it), and the test read the value alone: run
on its own it failed every time, back to at least 0.0.199. It now asks what it
claims - that the park waited.CI is green again, and runs what a change needs, once. It had been red since 0.0.200, and not for one reason.
std’s
runtime). A finished operation’s handle was replaced by its answer with a
plain assignment, which ran its Drop - the abandon its own comment
forbids - after the ring’s lock was released. Where another thread had
taken the slot in between, its operation was marked as nobody’s, reclaimed
unread, and answered the runtime lost a file operation’s slot. It showed as
a rare failure of fs::tests::appending_adds_and_create_false_refuses_a_new_file;
many_threads_reading_and_writing_at_once_each_get_their_own_answer failed
on every run before the fix and passes after it. A program at
user-parallelism = "yes" reading files from several threads could meet it.scripts/check-floor.sh set
CARGO_TARGET_DIR, every project build the tests start inherited it and put
the compiled std there, as it must, and
a_second_project_links_the_std_the_first_one_built looked in the shared
cache. The script passes --target-dir, and the project tests no longer hand
the variable on.NK1152: the depth limit counts calls,
and 128 calls of the build-time evaluator needed more than a test thread’s
2 MiB there. The evaluator runs on a thread of its own with a stack of its
own size, so the refusal no longer depends on which thread asked.push and as
pull_request; it now runs once, and a newer push cancels the older run. The
unsafe crates are checked when one of them changes. The floor runs on every
push to main, weekly, and on a pull request that changes a manifest, the
lock file or the toolchain (scripts/ci-changes.sh). Tests run with
--no-fail-fast, so a red run names every failure.A String field or result is below what flows into it —
ADR-222, the owner’s decision on
#200: the costs are carried by the special case, not by the
general one.
Right after parsing, every field declared String and every function declared
-> String is read for what the package puts there. With text of its own only,
it stays String and nothing changes. With views only (literals count as
views), it becomes ref String, and ADR-209 places the buffer. With both, it
becomes nikaia_std::either_text::EitherText, a Cow<str> that borrows a view
and moves text of its own in at each line. A published field or result is never
both; that one case keeps ADR-208 D2’s refusal. A name bound to a literal and
kept as text of its own is declared String. The README’s wall is gone for a
field and a return: a ref String parameter, a slice of a buffer the
function read (which used to pass the checker and fail in rustc), a name bound
to a literal, and return line.trim() from -> String all compile and run.
--tethers reports the tiers. A kept argument, an annotated let and ?String
are still refused as before (#200).
A list of structs of views that drops entries is held, one handle per buffer — ADR-221, the owner’s decision on the last open question (option 3).
The records program NK2304 refused compiles and runs: each element is a
tether::Holding beside the keep of every buffer it points into, the struct is
lowered once, and the compiler writes Views, Rebase and Hold for it. An
element is read through get (brackets, for, iter, first, last, get)
and a field written through with_mut; an element taken out with remove stays
held. A view is carried in by address — found in its keep, or copied where it
points into none, such as a literal — so the plan is checked rather than
trusted, and tether::hold, the one unsafe fn the emitted code called for a
held view, is gone. Two defects of ADR-209 D4 on the way: entries dropped only
after the loop no longer make a container a pruning one (it was refused for a
struct and paid a handle per view for text), and a removal bound by a let now
counts. A map of structs of views, a struct with type parameters and a view of
something other than text stay NK2304, by name. open-decisions.md is empty.
Edition 2024, and no .await point needs a change —
ADR-220, the owner’s question.
The migration lints found no if_let_rescope and ten tail_expr_drop_order
sites. Both changes make temporaries live shorter, and none of the ten holds
anything across a pause. crates/nikaia, nikaia-std, orchestrator and the
six crates/unsafe/ crates are on 2024; nested if lets became let chains, and
imports follow rustfmt’s 2024 order. The emitted code is 2024 as well, which
broke two things the emitter now writes differently: unsafe extern "C", and
gen escaped as r#gen. The floor stays 1.88, which scripts/check-floor.sh
confirms, because nothing needs 1.94.
The oldest Rust Nikaia works with is measured: 1.88 — ADR-219, the owner’s request.
The README and Part III promised 1.75 (ADR-109 D4). Building and testing with
each version found four things that need more: NonNull::map_addr (1.84, in
std), impl AsyncFn and async closures (1.85, in the emitted code),
std::io::pipe (1.87, in std’s tests) and the compiler’s dependency home
(1.88). The whole suite and all six crates/unsafe/ crates pass on 1.88.0.
rust-version = "1.88" is in every manifest, RUST_FLOOR (which every
generated Cargo.toml carries and the build checks) says 1.88, and so do the
README, Part III 13 and rust-toolchain.toml’s comment.
scripts/check-floor.sh installs exactly that toolchain and runs the suite on
it, and CI runs it as a job of its own, so the number cannot go stale again.
nikaia-std holds no unsafe — ADR-218
D4, the owner’s follow-up.
Five more crates under crates/unsafe/, each with its README, its unsafe
table and its checks: ref-or-box (an error’s tail in one word), checked-text
(UTF-8 checked once, chunked; a mapped file), c-text (a C string, returned by
an extern declaration as Option<CText>), polled (a poller that owns what
it watches), file-ring (io_uring with owned buffers, and a bell rung by
safe writes). A test’s mkfifo is the command. nikaia-std says
#![forbid(unsafe_code)] and no longer depends on libc, memmap2, polling
or io-uring directly. The emitted code’s unsafe (tether::hold, forever)
is what is left, as #84.
Every unsafe topic is a small crate of its own —
ADR-218, the owner’s decision.
crates/unsafe/<name>: one topic per crate, its own workspace and lock file,
a README listing every unsafe with its argument, and its own checks —
scripts/check-unsafe-crates.sh (fmt, tests and clippy on stable, Miri
under Stacked and Tree Borrows on nightly), run by a CI job of its own.
Building stays on stable.tether is the first: Keep, forever, Held, hold moved out of
nikaia-std (which re-exports them), plus Holding<V> — a struct of views
beside its buffer with a safe constructor — and Dangling<T>.Keep::put moved a Box it had handed references into (now
an Arc), and Held and the emitter’s __Tethered stored stretched views as
plain fields that promise validity across a call that may free their buffer
(now in Dangling).std::task::Wake: two hand-written RawWaker vtables gone.docs/open-decisions.md: option 3 is tether::Holding, no dependency.
#203 orders the remaining topics.The open question about structs of views names its crate. Option 3 on
docs/open-decisions.md — one handle per element — is built on self_cell,
which keeps unsafe out of the emitted code at the cost of one dependency with
none of its own. Tried by hand on the entry’s own program; yoke compared and
not chosen.
The open lists say what is open — the owner’s request.
the former backlog file loses the history of why it is laid out as it is: the
header, and the openings of §1 to §3, are the rules and the current order and
nothing else. #181 (the kept lowering of a function value) closed with 0.0.195
and is deleted. The question ADR-217 left — whether a container of structs
holding views that drops entries in a loop should be held rather than refused
(NK2304) — is on docs/open-decisions.md, with three options and a
recommendation.
What the specification stated and the compiler did not build — ADR-217, the owner’s request after 0.0.194.
T::variants: under [T: Enum] the loop is unrolled once per variant, in
declared order; a variant answers .name and .is(value), anything else is
NK1180. A call to a shape walk inside another call no longer renames both.let or a
parameter the callee keeps is one shared closure, nikaia_std::func::Kept.
Lambdas and named functions are wrapped where they are kept, a field holding
one is called like a method, pausing and failing calls make their function
pause and fail, a kept value handed to a run parameter is lent. NK1142 is
retired.fn make(name: ref
String) -> Reading returning a literal is accepted, and a store into a field
of a struct parameter that holds views ties the two with a named lifetime
(this reached rustc before). The two NK2304 refusals stay.fs::read hands back Bytes.The specification states the contract and nothing else — the owner’s decision that ADR references, rationale, history and implementation status cost every reader too many tokens and too much time, and are redundant with the ADRs, this changelog and the roadmap.
STYLE.md is rewritten to that rule, and the three parts follow it: no ADR-
citation is left, no Design rationale, no Implementation status note, no
withdrawn, since or today. Headings and section numbers are unchanged, and
every ```nika block keeps its code and its verdict (EXPECTED.txt
lists the same results); three blocks lost an ADR citation from a comment.
Rules that stood only inside a status note were kept as plain rules. The three
parts shrank from 64,392 to 47,182 words. NK1114 (removed from the compiler)
and NK1171 (a refusal of what is not built yet) are no longer described.
Also fixes the parts’ Date: line, which 0.0.193 left at the day before.
The text form of text is the text itself — ADR-216
D4, the owner’s why must to_string be a copy? and "nobody" is enough.
.to_string() on text now answers the receiver’s own type — a view stays a
view, a literal a literal — and is not written below. Only .clone() copies. A
bare literal is text of its own wherever it is kept, now also in a tuple’s parts
and after ?? inside an f-string hole; the latter uncovered a literal-position
collision between holes, fixed by keying hole literals by statement and hole.
The keep and tether analyses read to_string accordingly. Test programs,
examples and Part I write the bare literal. Open: whether a redundant
.to_string() on text should get a hint.
A copy has one word: .clone() — ADR-216,
the owner’s question whether .to_owned() still belongs to the language.
That a copy is written stays — it is the language’s promise that nothing
large is copied behind the program’s back. But it had three spellings and two
sets of advice: Part I named .to_owned() for text, which no ledger entry
described, while the ledger and NK2105 said .clone(). The language below
needs two words only because its .clone() of a reference copies the
reference; this language has no reference to copy, and since 0.0.191 the
compiler writes to_owned below itself where it is needed.
So .clone() is the copy, of text and of everything else, and .to_owned() is
NK1189, naming it. .to_string() stays: it is the text form of a value,
for every type. A clone of a slice hands back a list (to_owned below), and
the keep and tether analyses read a copy of text as a buffer of its own, as
they read to_owned. Every help text, Part I, Part III and both READMEs say
.clone(); the test programs that wrote .to_owned() were rewritten.
What was left of ADR-212 §5 — ADR-215, the owner’s yes to the last four finds, and a fifth found on the way.
HashMap::insert has an entry: the write under the language below’s
name, handing back the value it replaced, keeping key and value. A literal
where the receiver binds the parameter to String ($K) is built into
text there — the per-argument check had read the signature before the
receiver bound it.str::chars is Seq[char] sync ends, as String::chars was; it said
?, so nothing after it on a view of text was checked.ref xs[1..<3] is ref Array[T], ADR-184’s run —
the type a function declares to take one — and a list’s windows and
chunks hand that out too. Handed to a ref Array[i64] it runs; handed to a
ref Vec[i64] it is refused here rather than in rustc. A range kept in a
name slices exactly as a written one, literal ends included.fn copy(name: String) -> String { return name.clone() }
did not compile — the parameter is a &str below (ADR-207 D3) and .clone()
of a &str is the &str. A clone that goes to std is written
to_owned, a copy of a view and of a value alike; Vec::clone is new.A taking has a place in its statement and a path in its value — ADR-214, the owner’s fix the open points: ADR-213 §3’s three.
Within one statement. NK2702 and NK2105 ordered reads by statement, so
keep(name, name) and s.count() + s.count() went to rustc. Every read now
has its order in the walk, and a later read in the same statement is after the
taking. name = name + "x" stays a program: the assignment writes after it
reads.
Parts. A read of p.name is a read of that path. After xs.push(p.name),
p.x is still there; p.name and p as a whole are not (NK2105), and an
assignment to either gives it back. A part of something only lent — a ref
parameter, a for binding over a list, a let over a place — handed to what
keeps it is NK2106. And the probes found a wall under it: fn f(p: P) {
xs.push(p.name) } lowered p to &P and rustc refused the move; a part
handed to what keeps it now keeps its parameter, as return p.name did.
The stray warning. Every read through the brackets was (*get(…)), and
m[k] ?? 0 or let x = xs[1] came with unnecessary parentheses — the one
the README told testers to expect. The parentheses are written only where a
postfix needs them now, and a test compiles such a program and reads rustc’s
warnings back. The README no longer mentions it.
A key is not a position, and what is handed over is gone — ADR-213, the owner’s yes to repairing the first wall ADR-212 §5 found.
Maps whose keys they own. Every example keys its maps by ref String, so
nothing had written m["a"] = 1 or m[name] = 2 on a HashMap[String, i64] —
and neither compiled, nor did a read with an owned String, nor any key of a
HashMap[i64, …]: the brackets wrapped the key in index::at, which is for
positions and made the i64 a usize. Now a key written is handed to the
map as it is (a literal built into text there, a name handed over, a view
refused with ADR-208’s .to_owned() sentence), and a key read is lent. A map
keyed by views is unchanged.
Two more on the way. A literal assigned to a String — s = "b",
ids[k] = "seven" — was NK1105; an assignment keeps what it is given, so the
literal is built there as at every other such place (ADR-207 D1). And
m[k] ?? "-" over a map of text did not compile below; it is a view of text
now, with nothing copied.
NK2105: ADR-094 D2 decided long ago that a value handed to
something that keeps it and then used again is refused in this language’s
words; nothing did, and xs.push(name) then println(name) was rustc’s
borrow of moved value. It is refused now at every place a value is given away
— a kept argument, a key or value written into a container, a field, an element,
a let rename, an assignment — with .clone() as the way out, and inside a loop
or a lambda at the hand-over itself. Built on ADR-212’s machinery, and it found
that machinery’s blind spot on the way: a branch that ends in return or
throw takes its takings with it, which the http example’s
refuse(connection, …) return, three times in a row, depends on. NK2702
learned the same.
A sequence says what it is as a whole: a range replays, a pipeline walks
from its back end, and every taking is seen —
ADR-212. Outside feedback the owner
decided: the repairs first, and reversing a pipeline without a collect(); the
two-ended cursor left out on purpose.
The repairs. Four programs a tester writes in the first hour went to
rustc: a range kept in a name and walked (let r = 0..<3, for i in r —
no method named iter), a named keys() or drain() in a for (the same),
and a sequence taken by a let, inside a loop or inside a lambda and then used
again (use of moved value). A range now has a type — a sequence that
replays — and below it is nikaia_std::range::Span, a Copy value walked
as a copy, so it can be walked twice, backwards and in steps. A for over a
named sequence hands it over instead of writing .iter(). NK2702 now sees
every read that takes a sequence, and a take inside a loop or lambda of a
sequence from outside it — unless the loop gives it a new one or may leave.
And it learned the arms of an if and a match: taking in the then and
reading in the else was refused before, a correct program.
Three words after Seq[T]: ends (walkable from the back), sized (length
known), replays. On a receiver they are a demand — io::lines().rev() is
NK2703, in the program’s words — and on a result they pass through, with
ends_by_length for the adapters Rust walks backwards only when it knows the
length (take, zip, skip, step_by). Two words and not one, because
chars().rev() has no length and filter(…).zip(…).rev() needs one.
The pipeline: map keeps its element type (the lambda’s body binds $U),
and rev, zip, take, skip, step_by, a list’s iter(), windows and
chunks are new — all the language below’s own adapters, so a chain is one loop
and nothing is collected. A count is an i64 here and converted by the entry
the call resolved to, so a program’s own method called take is untouched.
Rust’s Range<i64> has no length, so (0..n).step_by(2).rev() would not have
compiled below; Span has one for every integer type.
New example: examples/trend/src/main.nika, three answers read from the end of a
list and a range walked twice. Found and left (ADR-212 §5): writing into a
map keyed by an owned String, HashMap::insert, str::chars -> ?, two takes
of one sequence within one statement.
What an error carries besides itself is one word — ADR-211, the owner’s objection to ADR-210 D3: every error paying for the special cases is not this language’s idea of cost.
It was worse than the list’s 24 bytes. A function that can fail returns a
Result, and a Result is as large as its failure half whether it fails or
not: Result<Int, Thrown<ConfigError>> was 96 bytes where the bare error
costs 16 — the site as a two-word &str, a 48-byte Option<Backtrace> almost
no process fills, and the list only an overlap fills. ADR-210 D3’s the
success path never sees an envelope was wrong, and is marked so.
Now the site, the trace and the list are one tagged word: odd, the site
alone, which is nearly every error and allocates nothing; even, a cold box with
the trace and the list, made only where a process traces or something joins.
The word is never zero, so a Result finds its niche there whatever the
author’s type: for a field-less enum, a unit struct or one Int of payload,
Result<Int, Thrown<E>> is 16 bytes — what the bare error costs. Site<E>
went from 88 bytes to 8, the boxed channel’s envelope from 104 to 24. The sizes
are asserted in tests; the unsafe that reads the word runs clean under Miri
with strict provenance.
Changed below: the emitter writes a throw’s site as &"load" (a
reference to the text, promoted like any constant) where it wrote "load".
Not done, and why (D3): the boxed channel is still two allocations per
throw. One would need the error inside a generic envelope, which a downcast
cannot find again — so either a failure channel type of this library’s own that
every Rust-side ? converts into, or a deprecated method as a marker. Both cost
more than an allocation on the failure path.
The joined failures are a diagnostic, not a value —
ADR-210, the owner’s answer to
open-decisions.md’s last question: option 3, against the page’s
recommendation of an opaque Failures type. Nothing is built, which is the
decision.
The secondary list exists because the language itself makes two failures
happen at once — the branches of an overlap, or a cleanup failing while an
error is already leaving — and a throw carries one; dropping the other would
lose exactly what an operator needs in an outage. That is built and stays: the
list travels with the failure, throw error keeps it, and the long form prints
it. What is withdrawn is reading it in code (throw LoadFailed(error,
error.secondary), ADR-115 D4’s example): nothing in the repository needed it,
and a type introduced for one example is a construct whose only reader is the
sentence that introduced it. If a program ever needs it, an opaque Failures
can be added then without breaking anything.
D3 writes down why every envelope carries the field though only two things fill
it: a failure travels through ? into a function that may join, and two
envelope types would convert at every boundary; an empty list allocates nothing
and only the failure path sees it.
Corrected on the way: Part I 7.1 and 8.1.2 said a handler reads
error.secondary. No such name was ever built. open-decisions.md is empty;
#199 loses the bullet that pointed at the question.
The tether is built: a buffer lives in the keep of whatever keeps its views, and the compiler decides which — ADR-209. The first of the README’s design risks, taken in one step as the owner asked: the programs that need it were invented first, the design was checked against them, and all of it was built.
struct Setting {
key: ref String,
value: ref String,
}
fn load(path: String) -> Vec[Setting] throws {
let text = fs::read_to_string(path, fs::Root::Anywhere)
… // cut `text` into settings
return settings // views of `text`, which this function read
}
was refused (NK2303, or NK2302 blaming path, or rustc about a file
nobody wrote). It compiles and runs, and nothing is written for it: no
lifetime, no annotation, no copy.
Eight programs, one per shape — a loader, a view handed two frames up across a pause, a loop over files, recursive includes into one map, a task, a cache that drops entries, the loader with no word above it, one line of a mapped file — said that ADR-008’s one representation (positions, three layouts, a buffer table) was one answer where the facts differ. So:
&str, checked by rustc.views = ["<result>: tethered"]), so it
crosses package boundaries.get whose shortening
rustc checks.Held, compared by content), so a buffer is freed with its
last view instead of living as long as the loop.@tethers. ADR-201 D2
made a tether an error without the word; built, that was a refusal that knew
exactly what to do — the lifetime-annotation tax under another name. The
decision is the compiler’s, like Rc or Arc, and nikaia --tethers says
where each buffer lives and why.for,
push/insert, calls and f"…" holes; a buffer handed on whole is a move.NK2302 blamed a parameter for a view of text read from it
(fs::read_to_string(path) hands back a String, not a piece of path).m["host"] ?? "-" over a map of views: type annotations needed from rustc.NK2303 twice, once as a place that can fail.NK2304: one buffer handed both to a task and out of the function (no one
owner), and a container of structs holding views that drops entries in a loop.
nikaia_std::tether (Keep, Held, and two unsafe functions with their
contracts), contracts::keep (the plan and its fixpoint), the emitter’s keeps,
tests/tether_keep.rs (the eight programs at both settings), four bytes.rs
tests and two tethers.rs tests moved from the refusal to the tether. Part I
6.6, Part III, ADR-008/156/201’s status, #196 (the tether’s entry is
closed) and the README follow.
A view of text is asked for a copy only where something keeps it — and the refusal says why — ADR-208. The owner’s point after 0.0.183: a language that hides ownership this thoroughly owes the reason at the one place it does not, or the reader asks why a compiler that knows exactly what to write does not write it.
fn relay(city: ref String) { show(city) } for a show(s: String) that only
reads asked for city.to_owned() — a copy nothing would keep, since the parameter
is a &str below (ADR-207 D3). D1: a view handed to a String the callee
only reads is lent as it is, for a function this compiler declares; a ref the
source writes there is still NK1137.
Where something keeps the text — a field, an annotated let, a return or a
body’s last value, an argument the callee keeps — NK1102/NK1103/NK1104/
NK1106 now say whose text it is, what keeps it, and what a copy made on its own
would cost (ADR-005 §3), in three cases:
error[NK1106]: `Reading.name` is `String`, and this is `ref String`
= `city` is declared `ref String`: the text belongs to the caller, who still has it
= `Reading` keeps its `name` after this line, so it needs text of its own
= Nikaia copies text only where the program says so: a copy costs as much as the
text is long, and one made on its own would run every time this line does, with
nothing in the source to show it (ADR-005 §3)
help: declare `city: String`, and the caller hands its text over instead of
lending it - or write `city.to_owned()` to copy it here
A name bound to a literal gets the honest not yet: written in place the literal would be built on that line (ADR-207); through a name it is not, because that would change the type the name was declared with — and the help names the two spellings that work. Any other view says it points into text something else owns.
a_view_kept in the checker, read by every keeping position; a binding remembers
the literal it was bound to. Two new tests in tests/text_literals.rs — D1 run
end to end, each explanation checked. Part III C.4’s example is the new message.
A text literal is a String where one is wanted —
ADR-207. The first refusal a pre-alpha tester
met, and the owner asked for it gone with the complexity in the compiler: clever
rather than complicated, and not more expensive than it has to be.
Person { name: "Ada" }, let s: String = "x", return "x" out of a
-> String, greet("x"), a T? field given a literal, let xs: Vec[String] =
["a", "b"], and a match whose arms are literals where a String is handed
back — all compile now. So do the mixed forms, because a literal takes its
neighbours’ type: ["a", f"c{n}"] is a Vec[String], and
if c { name } else { "anonymous" } over a name: String is a String.
ADR-107 D1 decided this and routed it
through a new representation of text — a handle of three states on every text
value. That costs every value a wider shape and a branch, to save an allocation
in one position, and the state it adds is the one the tether analysis found
nothing in the corpus needs. Inserting .to_string() everywhere is the other
cheap answer and is wrong where the callee only reads. So the answer is per
position, off two things the compiler already knows — what type the
position wants, and whether the callee keeps the parameter (ADR-094):
String::from("…"),
as [1, 2] is vec![1, 2] where a Vec is wanted (D2). The cost is exactly
what the program paid when it wrote .to_string().String parameter the body only reads is a &str below now,
not a &String (D3), and the literal goes in as it is: nothing is allocated.ADR-005 §3 stands. A view of text the program has is still refused where it
is kept, and NK1106/NK1102/NK1103/NK1104 now say how out of it —
write .to_owned(). They used to say make it a String: the arm with the
right sentence matched &str, which the checker stopped printing when the
spelling became ref String.
keeps (ADR-094) counted a parameter as kept only behind a written return or
an assignment. So fn same(name: String) -> String { name } — the shortest
program that keeps anything — lowered to &String and failed in rustc with
mismatched types about a file nobody wrote, and so did a parameter handed out
through an if or match arm or through let s = name; return s. D4: the walk
reads a body’s last expression as a return, descends through if, match
and blocks, and follows a let’s second name. A let that is only read still
keeps nothing. No committed ledger changed.
Expr::LitStr carries its position, for ListLit’s reason; owned_texts
goes from the checker to the emitter..to_string()s gone from examples/ (http, hello-http,
sqlite, fortunes); the eight left copy a view, convert a foreign pointer,
or are foreign-runtime fixtures that measure something else.tests/text_literals.rs (every position, compiled and run, and the refusals
that stay) and two new tests in tests/keeps.rs; four existing tests moved
from asserting the old wall to asserting the new shape.A name bound to a literal is a view — let s = "x" then Person { name: s }
is refused with the .to_owned() help; making a binding follow a later use is
inference across statements, and a larger question. A method’s arguments are
not lent yet, so a literal there is constructed even where the method only reads.
A route is not refused by what its handler touches, and no report lists it
either — ADR-206, the owner’s answer to
open-decisions.md’s last remaining 0.0.174 question.
Nothing is built, which is the decision, and no
the former backlog file entry is created either: option 1 is the
answer and not an unbuilt feature.
Every function carries a touches column, closed over what its body calls. So
the compiler already knows whether a handler reaches the filesystem, transitively
— which no other language’s http.server knows, because no other one has the
column. What to do with that was the question, and it named three options: refuse
nothing, let a route declare what it may reach, or list it in
nikaia --trust.
D1 — no refusal. No reaching: at a route, no word for it, no code. A refusal
on today’s granularity would mostly refuse programs that are right, and touches
says file(path) read where the path is a parameter’s name and not a value.
D2 — and no listing either, which is the half the page recommended and got wrong. The owner’s reason decides it: in a real application almost every route reaches something, so a report whose rows are nearly all non-empty tells a reviewer nothing they can act on. It is the argument this project already makes about a gate — a gate that is a fifth false alarm teaches people to ignore it — in the other tool.
A listing is not free. The page called 3 the cheap direction because the data
a later refusal would read has to be gathered anyway. What it did not count is the
design: which touches are ordinary for a handler and therefore filtered, and
which a person asks for on purpose — --extended, --including http, a name
nobody has picked. That is a question with its own answer and not a 0.1.0 target.
Option 1 costs nothing and stays extendable.
D3 names the distinction the page missed. ADR-108 D4’s listing works because it lists a way around a check: such a list is short by construction and prints none for a program with no exceptions. A list of what every handler touches is long by construction, and a report that is long by construction is read once. So a report earns its keep the way a refusal does — by what it leaves out.
The question’s entry gained a paragraph claiming fs::Root’s arrival was option 3
arriving for the filesystem. It was not: one report lists exceptions and the other
would list the ordinary case, which is the difference between them rather than a
step from one to the other. This is the second time an answer has corrected how
that page framed its own question
(ADR-195 D5 was the first), and both are the
page working: an entry written to be answerable is one whose framing can be argued
with.
ADR-206 §4 writes it down so it is not found again: the filter, the switch, the
better granularity fs::Root already gives for one resource — and that a refusal
stays available, because D1 says no to it on today’s granularity and nothing
closes the door the root opened one resource at a time.
A bound reaches a caller inside the signature —
ADR-205, the owner’s answer to
open-decisions.md’s second question, in favour of that
page’s recommendation. #162 closes and §1
is empty.
A call into a package’s generic function was not checked against its bound:
error: app/src/main.nika:6:5: the trait bound `Bare: Handler` is not satisfied
= the trait `Handler` is not implemented for `Bare`
= the trait `Handler` is implemented for `Static`
The position is right and the words are the backend’s, which is Part III
C.1. Handler is written without the
path the program must write, and Static is a name the program never mentions.
The check was four lines. What was missing was the key: the bound lived only
in the AST of the unit that declared the function, and the ledger had no column
for one — a signature wrote (h: $H) -> String and the : Handler was nowhere
in it.
[fn."dispatch"]
signature = "[H: Handler](h: $H) -> String"
In front of the parameter list, and nowhere else. The signature already
carries the type parameter as $H
(ADR-074 D2), so a bound is the rest of that
sentence; a second key that has to agree with the first is a second source of
truth for one fact. [T] for a parameter with no bound is written nowhere, because
the $T already says it exists.
Additive. The list stands before the (, so a signature written before this
key existed parses unchanged — and no ledger in this tree changed a byte, because
nothing in it publishes a generic function with a bound. That is what #162 meant
by the first package to use it is the one that meets this.
A package writes its own names bare and a consumer writes the path, because naming
is per package (ADR-047 D1). Every prefix of
the entry’s key is tried and the first spelling this program’s trait map knows
wins. Without it the refusal never landed: the map held handler::Handler, the
bound said Handler, and the check found no trait and passed the argument through.
Where no spelling matches, the name stands as written and the fail-open line does
what it always did (C.4).
error[NK1164]: `handler::dispatch` asks for a `handler::Handler` here, and `Bare` is not one
= `handler::dispatch`'s `H` is declared `[H: handler::Handler]`, so the type a caller
picks has to answer for `handler::Handler`'s methods - and nothing in this program
says `Bare` does (Part I, 4.7)
help: write `impl handler::Handler for Bare { … }`, or pass a type that already has one
The path is in it and the help is a line the program can copy. The hash covers the bound, because it covered the signature already (ADR-100): a bound added or removed changes what a consumer compiles against, and now it changes the file too.
Whether a type compares is a question about its parts — ADR-204, and #157 closes the same day it opened. §1 holds one entry, and it is the one waiting on a decision.
error: binary operation `==` cannot be applied to type `P`
= an implementation of `PartialEq` might be missing for `P`
= consider annotating `P` with `#[derive(PartialEq)]`
on the author’s line, in the backend’s words about a file nobody wrote, with a
help the source cannot take — Part III C.1 and
C.2 at once. Every emitted type derived
Debug and Clone and nothing else.
0.0.179 took the half that needed no decision: an enum whose variants hold
nothing has no part that could fail to compare. This is the rest, and it
subsumes that rule rather than sitting beside it — such a type has no parts,
so the walk answers yes for it by the same sentence it answers for every other.
The one ADR-005 §1 Group B already asks for may this cross a thread, pointed at a different question:
bool, char, String, Bytes — and a container exactly when what it holds does;struct from its fields and a declared enum from its variants’ payloads, recursively, with a type that holds itself answering once rather than forever;compares column, and absence is not permission (ADR-010 D1): a lock, a mapping, a socket, a task’s handle and a channel’s ends do not compare. compares = true is a claim reviewed like code, which is what crosses = true already is and for the same reason.Eq is the same walk with one primitive answering differentlyNaN != NaN, so a float compares and is not an equivalence — and every type
holding one differs with it, however far down. examples/json/src/main.nika is what said
so: Json::Number(f64) made Json PartialEq, and a struct holding a Json
was derived Eq beside it, which rustc refused about the generated file. A
look at the type’s syntax would have missed that; the recursion does not.
A library type is never taken for an equivalence, because whether one is, is a
second claim nobody has had a use for. So a struct holding a time::Duration
compares and is not a map key yet — the under-approximation that cannot be wrong.
Nothing had recorded one. A Named variant’s fields were in the checker’s map
under the qualified key; a Tuple variant’s were in no map at all, so
Number(f64) looked like a variant holding nothing. The map that now holds them
is what the Eq question reads.
NK1188== on a type that does not compare is refused here, naming the type and what
decides it, with a help a program can take: compare the parts that carry the
answer, or give the type a method that says what equality means for it. There is
no #[derive] to write, which is what made the old help a way out that cannot be
taken.
The NK table gains it — and NK1187 too, which the compiler has emitted
since 0.0.168 and the table never listed. A stale status note is a defect in its
own right (README.md §1: a reader cannot tell a plan from a
promise).
An enum’s cases are a column of the ledger —
ADR-203, and with it
ADR-018 D4’s Method, which is the last of
that record’s data.
A [type."X"] entry carried a struct’s fields and nothing at all for an
enum — an enum had no entry. So a consumer of a package could not see
that the cases were Yes and No, and
match verdict::of(word) {
verdict::Answer::Yes => { println("yes") }
verdict::Answer::No => { println("no") }
}
was NK1151, this match does not cover else. Two things wrong at once: a
correct program refused (Part III C.4),
and a way out that is the one ADR-146 D1
exists to prevent — an else makes a type gaining a variant silent in that
program forever after.
variants = ["Yes", "No"], one line per case as the source writes it: a bare
name, a positional payload Name(T, U), or named fields Name { a: T }. A
struct has fields and an enum has these, and neither has the other’s. Read
from every ledger, with this unit’s own source winning.
A column and not a record of its own, because it is the same fact as fields
for the other shape of type, in the same table. There was no second credible
spelling — which is what makes it work rather than a question. How does a bound
reach a caller across a package boundary? stays on
open-decisions.md, because that one has two.
== on a unit-only enum. k == Kind::Post reached rustc as binary
operation == cannot be applied to type Kind, with consider annotating
Kind with #[derive(PartialEq)] as the help — the backend’s words about a
file nobody wrote, and a way out the source cannot take. An enum whose variants
hold nothing derives PartialEq and Eq now, which is a derivation rather
than a caution: such a type has no part that could fail to compare. == on a
struct and on a payload-carrying enum is #157, with the shape
a fix takes and the reason a blanket derive is the wrong one — it would refuse
the declaration of every type holding a lock.
match error { … } is a match over the type that arrived, where exactly
one does. It was non-exhaustive patterns: IoError::NotText(_) not covered,
about the generated file. The binding itself stays untyped, so this can only
move a refusal and never add one: where the arms cover everything, nothing
was said before and nothing is said now. Where several error types arrive the set
is open and else is still the only answer
(ADR-160 D4, untouched).
Methodpub enum Method { Get, Post }
in examples/http, and request.method() answers it. Two variants are the
whole type, and that is std’s refusal rather than an omission: http1
answers 400 to any other method before a handler runs. method_text() is
beside it for a log.
A path names its root at the call — ADR-108, whose §5 said nothing and now says D1 through D4. #189 closes.
The hole it shuts is path traversal: a name somebody else chose, joined under a
directory, reaching the filesystem and leaving the directory through .. or a
symlink. The record’s reasoning is why the check is in the call rather than a
taint per value: a programmer sees a missing argument and not a taint, the check
is std’s so checked and correctly checked are the same thing, and the way
around it is one word that a report lists.
let data = fs::map(name, fs::Root::Dir(store)) // name may not leave store
let text = fs::read_to_string("eins.txt", fs::Root::Anywhere)
fs::write(out, fs::Root::Dir(site), markup; append: true)
fs::Root, with two variants and no shorthand (D2). Not a bare String where a Root is wanted, which would be a conversion rule this language does not have; not a second spelling beside the variant.NK1101 with D2’s two forms in its help.map, read, read_to_string and write take it. open and the directory functions of D1’s list are not built, so they take it the day they are.By component on a resolved name, never as a string prefix (D3), because
/data is a prefix of /data2 — which is a case in the test rather than a
sentence in a comment. Both sides are canonicalised, so a symlink out of the root
is the same answer as a .. out of it. A name whose tail does not exist yet is
checked all the same: every component that exists is canonicalised and the rest
applied on top, which is the half a plain canonicalize would have got wrong on
exactly the call that most needs it — a write that creates.
What comes back is the name joined under the root and not the resolved one,
which is D3’s never a rewritten name. sub/../ok.txt where sub does not
exist resolves onto ok.txt; handing that to the operating system would serve a
file that an open of the written name does not find. So the check answers
inside, the operating system answers not found, and each says what it knows.
The refusal is io::IoError::Outside(what) — D3’s one case beside not found
among the errors the entry already names. A case and not an error type of its
own, so no program’s throws set grows a member, and the payload is what was
asked for rather than where it landed: a message that printed the resolved name
would tell whoever sent it what is on the machine.
nikaia --trust grows D4’s third place: every site that writes Anywhere, and
every Dir("/"), which is the same thing in more characters — by file and line,
because the security review of a program’s file access is that list and a list
without line numbers is not one. A Dir the program worked out is not listed:
the report is the list of ways around the check.
Which argument is the root comes from the ledger, not from a list of function
names in the compiler: an entry’s signature names a parameter root. So the day
fs::open or http::File is written, its sites are listed without a line
changing there.
Fifteen calls in examples/ and benches/, every one of them a command-line
program whose path the operator typed, so every one of them honestly
fs::Root::Anywhere — an operator’s own argument may point anywhere. The
specification’s own blocks and examples/README.md are written the same way.
A path in an argument is a constant, and the ordering analysis said it was a value read from somewhere else. That sentence is true of a field and of an index and not of a path: a path names an item — a unit variant, a constructor handed over as a value (ADR-140 D2), an associated constant — so there is nothing to read, nothing for an overlapped branch’s closure to capture, and nothing to carry to another thread.
Nothing wrote a path in an argument until every path call took a root, and then
the over-approximation took the control pair of
ADR-038 §4.5’s D7 experiment with it: two
reads of two files, which have to overlap or that test proves nothing about a
foreign call. So the narrowing arrives with the root rather than on its own, and
overlap { } over three reads is what holds it there.
A view of the subject is handed back without a ref —
ADR-202, and
ADR-094 D1’s third position.
#151 is closed, and §1 holds one entry.
pub struct Holder { text: String }
impl Holder {
pub fn text(ref self) -> ref String {
return self.text
}
}
was two refusals at once — NK1131 self is borrowed here, so text
cannot be handed back by value, and NK1104 this returns String, and the
function declares ref String. Both are about one missing character, and
neither named it.
And the one spelling that worked was the one the author should not write.
return ref self.text compiled. So the state of the language was a reference
the author writes is required in one position and refused in two others, which
is what D1 exists to end — from the side nobody had looked at.
& at a return, where the function declares a view and the value is a place inside a borrowed subject. The tail form is the same program: fn text(ref self) -> ref String { self.text } and the return form lower to one file.NK1131. A receiver taken by value has nothing to lend — the subject dies at the end of the call — so that one is NK1104, in this language’s words rather than rustc’s about a file nobody wrote (Part III C.1).let position’s already does.borrowsA method whose result is a view and whose receiver is ref self records
returns = "borrows(self)". It always belonged there —
ADR-098’s column names the positions a
result may point into, and a ref self is a view the caller gave. It was left
out while nothing read the column across a receiver; the reader that does
arrived at 0.0.176, and read no position at all for the accessor a program
most often writes.
NK1131’s third way out, and only where it can be takenThe help names declare the result ref T and the view is what this line means
where the field is handed back, and not where it is bound or passed: those
have nowhere declared to point, so ADR-083
D2’s two ways out are still the whole answer there. Offering a third would be
Part III C.2 from the other side.
The written ref is not refused, and one root still needs it (ADR-202 D2 and
§4, #96). D1 writes the & where the place is inside a borrowed
subject and not where it is inside a lent parameter — fn a(row: ref Row) ->
ref String { return row.name } still wants the word — so the two are one piece of
work: the inference reaches every root, and then the second spelling goes. D4
already stages its own refusals for this reason.
examples/http’s Request::body() is written, which is
ADR-018 D4’s last accessor — the request’s
whole surface is methods now.
A call says which of its arguments its result may point into, and the view
analysis now reads that column. #152 is
closed, and with it ADR-018 D4’s last two
accessors: request.query(name) and request.header(name) are written on the
request itself rather than reached through the head it holds.
views::analyse took no ledger. A view parameter handed to a call whose result
is returned was treated as escaping through that result, so
pub fn query(ref self, name: ref String) -> ref String? {
return self.head.query(name)
}
was NK2302 — and the program is correct: http1::Head::query is written
returns = "borrows(self)", so its result points into the head and never
into name. A correct program refused, which is Part III
C.4. The way around it was not open
either — declaring the parameter String refuses every caller’s literal, which
is ADR-107’s text is one type, unbuilt.
The two ledgers, at the call. returns = "borrows(…)" names the positions the
result may point into (ADR-098), and an
argument that is none of them does not reach the result through it. The receiver
is one of those positions, and self is what names it.
By the whole key and not by the method name, which is the mistake worth
recording: a wrapper keeps the name it wraps, so a lookup by name alone let
Request::query find itself among the candidates — and its own column, the
very thing being decided, answered for the callee’s. The receiver is typed from
what the source wrote down: the subject, a field of the subject, or a parameter.
Everything unwritten reads as nobody said — no entry, an empty column, a receiver this walk cannot type — and the refusal stands there. That is ADR-010 D1’s polarity: an analysis that cannot see is not allowed to conclude that nothing happened. A local’s type is the checker’s business and this walk runs beside the checker rather than after it, so a call on one still refuses.
#151 — a method cannot hand back a view of a field it owns —
is untouched by this: that one is about the & at a return, not about what a
callee’s column says. examples/http’s body is still a pub field and still
says so where the accessor would be.
A handler can ask its request — ADR-018 D4’s surface, as far as the language reaches, which #105 said none of it can be built before there is a server to bind to. There has been one since 0.0.166.
std::http1 keeps what the parse was throwing awaycontent-length and transfer-encoding and dropped the rest, so a handler could not ask. Head::header(name) is case-insensitive, as the protocol is (D4) — the names are lowercased on the way in, so a lookup lowercases one word and nothing else happens per call. Where a client sent the same name twice it is the first: joining them with a comma is what the protocol says a list-valued header means, and which headers those are is not something this module knows.path() and query() are two things (D4), so path() no longer carries the query string and target() is the whole of what the client wrote, for a log. query(name) answers nothing rather than an empty string for a name the client did not write — and an empty one for ?debug, which says the name was written. Nothing is decoded: a %20 stays %20 and a + stays a +, because which of the two a + means depends on who wrote the form, and std guessing would be guessing on somebody else’s bytes (ADR-010 D2).Vec of pairs and not a map: a request head has a handful of headers, and there is nothing here to measure yet — which is itself the reason to take the shape with no allocation behind it.Head foundBoth rustc about a generated file (Part III C.1), both in std:
Head was not Clone. Every emitted Nikaia struct derives it, so a std type a program puts in a field has to — the trait bound Head: Clone is not satisfied.header(&str) could not take what a Nikaia method hands it. A method’s argument is passed owned (ADR-094 §5: the compiler cannot yet resolve which entry the call goes to), so a String reached a &str. impl AsRef<str> now, which is net::Connection::write’s reason and what the ledger’s ? has always meant.NK1131 said declare the result &str and write return &self.text. Both halves are impossible: &str stopped being a spelling at ADR-184 D4, and a & a program writes is NK1137 since ADR-094 D1 — and doing what it asked gets NK1131 and NK1104. That is Part III C.2, a way out that cannot be taken is not one. The message names ADR-083 D2’s two ways out now — .clone() and a self receiver — and says outright that a view is not a third.
& at a return, which is D1’s the compiler writes the reference in a third position, beside the declaration and the call — and returns = "borrows(self)" is the column that already records it.views::analyse takes no ledger, so it cannot read that http1::Head::query borrows its receiver and never its argument. Fail-closed, which is the right polarity for an analysis that cannot see — and the answer is written down one file away. The way around is not open either: an owned String parameter refuses every caller’s literal, which is ADR-107’s text is one type, unbuilt.the_corpus_has_no_more_unanswered_method_calls_than_it_had: 62 → 63, and the twelfth is the eleventh again one accessor over — request.head.query("name") in the same handler, on the same lambda parameter, whose type is the package next door’s and which a one-file sweep cannot see.&str. A better spelling for a way out that does not exist is not better, so that test is now its opposite: the help names only what a program can write, whatever the field’s type is.Both block D4’s request.query(name) and request.header(name); both are reachable meanwhile through the head the request holds, which examples/http says in a comment where the accessors would be. crates/nikaia/tests/project.rs drives the query and the header over a real socket, asked in a case neither the client nor the handler wrote.
The questions go where questions go — a pass over the former backlog file against its own rule, citing a question is not asking it, which is a failure that file has now had three times and the third was written two versions ago.
the former backlog file’s head says it outright: an entry that names
a question elsewhere is not finished until the link resolves, and it says the
failure was found twice before, at 0.0.150, by following its own links. #162
made it a third time. It said the ledger has no column for a bound and that
whether that is a new key or a widening of the signature language is ADR-106
D3’s table to extend, and no record does it — naming a decision and writing it
nowhere.
bounds key of its own, the bound inside the signature string where the declaration writes it, or leaving the refusal to rustc — with a recommendation (the signature, because it already carries the type parameter as $H and a second key that has to agree with it is a second source of truth) and what each costs if it is wrong.throw LoadFailed(error, error.secondary)). Three options — an opaque Failures in std, dyn as a type, or neither — with a recommendation (opaque, because what a program does with a list of failures is count, walk and print it, and none of that wants an element type; and because dyn is a large decision to take under the pressure of one example). That bullet named the question for two versions and wrote it nowhere.fn tell[T: greet::Speaks](x: T) waits on a module question nobody has decided. ADR-078 §4 did leave it open and ADR-106 answered it; the door opened at 0.0.171.Nothing in the compiler changed. open-decisions.md holds three questions, and every question the former backlog file names now resolves to one of them.
A field handed back out of a parameter keeps it — #156 closed, the package after the one that found it.
pub struct Answer { pub text: String }
pub fn say(answer: Answer) -> String {
return answer.text
}
lowered to pub fn say(answer: &Answer) -> String { answer.text }, and rustc
answered cannot move out of answer.text which is behind a shared reference
about a file the author never opened — Part III C.1.
Answer over and never uses it again is exactly what the keeps column is for (ADR-094 D1). So hand_over — which read a bare name, so return answer kept and return answer.text did not — now reads a field of a parameter too, and the declaration is written by value as it already was for the whole value.NK1131 one position over. That refusal says this exact sentence about a ref self subject, and asks it of self alone because until D1 nothing else was lent without the word. Here the lending is the compiler’s, so the lending is what changes.return point.x for an i64 takes nothing away, and owning the parameter for it would take the value from a caller that still wants it — a correct program refused, one call up (Part III C.4). Measured both ways: across(p) then p.y on the next line compiles and prints 34.self is not this rule’s, deliberately: a ref self is a word the author wrote, so what a method does with its subject may not silently turn it into fn(self). A test holds that line.keeps_its reads an unknown callee as one that keeps, and an unknown field is the same question one level in. The cost of guessing the other way is rustc about a file nobody wrote; the cost of guessing this way is one value the caller has to .clone().One entry: #162, a call into a dependency’s generic function not checked against its bound, which needs a ledger column no record decides. The five before it were each found by running something, and the last three by running what the package before them had just made possible — which is what this section’s method produces.
A dependency’s unit is checked in its own namespace — [#159](https://github.com/Nikaia-Language/Nikaia/issues/159) #159 closed, one package after the one that found it.
pub fn dispatch[H: Handler](h: H) in a package compiled on its own and was
NK1126 — nothing says it has a method — the moment a program depended on it,
without the program calling it. Every unit of a build is checked, and a
dependency’s was checked against the program’s ledger, whose keys absorb
had qualified: Handler had become handler::Handler, and the package’s own
file writes the bare word, as its author must
(ADR-046 D2 gives no import to write). A
correct program refused, which is Part III C.4.
Program::as_its_own keeps each package’s ledger as that package was inferred with it, and the checker reads it while that package’s units are walked. Kept rather than reconstructed.examples/http’s own answer(connection, handler) was told that its fn(Request) -> Response is not a fn(http::Request) -> http::Response, and render(answered) that a Response is not a ref http::Response. The suite caught it. An inverse of absorb that is nearly right is worse than none, which is why the ledger is kept and not rebuilt.handler::dispatch(Bare { n: 1 }) comes back as the trait bound `Bare: Handler` is not satisfied — rustc’s words on the author’s line, naming Handler without the path the program must write and Static, a name the program never mentions. Checker::declared_bounds is built from the AST of the unit being checked, under the key a call resolves to (dispatch), and the call writes handler::dispatch. The ledger has no column for a bound, and whether that is a new key or a widening of the signature language is ADR-106 D3’s table to extend — a decision, not a patch.rustc’s to refuse. pub fn say(answer: Answer) -> String { return answer.text } lowers to &Answer and cannot move out of answer.text which is behind a shared reference. It is NK1131 one position over: that refusal says this exact sentence about a ref self subject and asks it of self alone, because until ADR-094 D1 nothing else was lent without the word. The answer is not a refusal — the program is correct — but a widening of the keeps inference: hand_over reads a bare name, so return answer keeps and return answer.text does not.Both were found by building what the package before this one had just made writable, and then by writing a fixture for it. §1 holds two entries and §2 holds 33.
A bound takes a path, and the ledger records traits and impls —
ADR-106 D1, D3 and D4, which closes
#193.
fn dispatch[H: handler::Handler](h: H) parses, and [T: A + handler::B] mixes a local bound and a foreign one. generic_bound takes type_name, which is the record’s own sentence — the grammar’s rule for a type name is the rule for a bound’s trait name — so the whole path is interned as one name, exactly as impl handler::Handler for Fixed already had it.:, expected one of: +, ,.[trait."Handler"], and it carries nothing else: a trait’s methods are the fn entries beside it, under Handler::handle, which is the key shape an impl’s get and the one NK1130 already compares against. Writing them twice would be a second source of truth for one fact, so the set is filled from functions once the whole file is parsed.[impl."Handler for Static"], one line per pair, in the ledger of the package that wrote it (D4). An impl may stand in the trait’s package, in the type’s, or in a consumer for its own type, so no single ledger can list a trait’s implementors completely — and a list read as complete would turn absence into an answer, which ADR-010 D1 forbids. The answer at a call is the union over every ledger the program reads plus its own.impl under both spellings, the package’s and the qualified one. implementations already widened the type that way and keyed the trait unqualified, so a bound written handler::Handler looked the answer up where it was not. An extra spelling can only make the check fail open, which is Part III C.4’s side — and the refusal it must not take away has a test of its own.ADR-078 §4 left a trait a package publishes as a question about modules, and that sentence outlived the record: #193 repeated it as nobody has decided until 0.0.170’s pass, and the doc comment on Ledger::traits carried it too. ADR-106 had decided it; what was missing was the build.
Three tests in crates/nikaia/tests/project.rs, each two packages, because inside one the module’s names are the package’s and a bound needs no path at all: the consumer’s own impl, the package’s impl, and a type that implements neither.
#159: a package’s own bound is refused when the package is a dependency. pub fn dispatch[H: Handler](h: H) in a package compiles on its own and is NK1126 — nothing says it has a method — when a program depends on it, without the program calling it. A dependency’s unit is checked against the program’s ledger, whose keys were qualified while it was absorbed, and the dependency’s own file writes the name unqualified as its author must.
Reproduced with this package’s change stashed, so it is pre-existing and not the path’s. The cheap fix is the wrong one: giving traits both spellings would make a program’s own [T: Handler] resolve to a dependency’s trait, which is a second spelling of a name ADR-046 D2 says must carry its path. What the case wants is a dependency’s unit checked in its own namespace, and that is a question about how the units are walked.
A pass over the former backlog file, closing what is answered and
saying what is blocked — no compiler change, one documentation comment, and
four entries that were telling a reader something that had stopped being true.
docs/README.md §1 makes a stale status note a defect in its
own right, because a reader cannot tell a plan from a promise.
threads says true is NK2502 in this language’s vocabulary, and the test that asserted the silence has not existed since. Deleted; the number stays where it was, because this file is cited by number.unsafe impl Send flag, the call graph and the use table — and so is the parser under it (ADR-195 D3, ADR-196 D1). The entry is now what is actually left, which is not that record’s: cargo metadata, and the fs directory walk and std subprocess it runs through (ADR-195 D4). Its number is kept because two records cite it by one.[trait."http::Handler"] table whose methods are ordinary fn entries. So it is work and not a ruling — which is the whole distinction between this file and open-decisions.md. The same stale reasoning sat in the doc comment on Ledger::traits and is corrected there too.else if parses and lowers and the entry was still narrating it. What is left is that there is no formatter, with one rule already written down for the day there is; the entry says that and nothing else now.§1 is empty and §2 holds 34 entries. Three closed in the last three packages — use std::<anything> (#146), the grammar action block (#202), and the described call’s threads (#195) — and this one is the pass that makes the rest say what is true.
A grammar’s action is the block after the pattern, and two borrowed names go — ADR-120, built whole, which closes #202.
rule NAME -> Type = pattern { action }. The one -> a rule writes is its result type, as a function’s is; the arrow in front of the action was a wart from the days when the two sat on one line (D2).examples/, 153 in crates/nikaia-std/src/tools/rust.nika, the rest in the fixtures and the error corpus — plus 87 more in the grammars the tests embed as source strings.{: what a reader of a program written a version ago gets is a sentence and not a token.fixtures/measurements_expected.rs is byte-for-byte unchanged across this whole package. The surface and the engine’s own spelling are allowed to differ, as they already do for dec[i64].NK1135 for the type it names and does not declare, which is the honest answer for a fragment.digit{1} is a bound and digit { 1 } is an action. D2’s a brace bound starts with an integer was nearly right and is now true: with the action being the block after the pattern, a rule that yields a number is indistinguishable from a repetition by a digit follows the brace alone. G_BOUND_START is lexical, so nothing is skipped inside the lookahead.tag and digit1 are NK1187tag("x") is "x", and digit1 is digit+, which yields the text it matched as every character-class repetition does.digit1 reached the engine, worked, and the page it is not on said nothing. D1 makes Part II 10.8 the whole vocabulary — an element that is not on the page is not in the language — and a second way to write one thing is what that rule exists to stop.C2 was rule A -> i32 = n:digit1 { n } — an action block whose arrow was forgotten, read as a repetition bound, answered with expected a digit. That input is now the form. The row asks the same reader question from the other side: it is the old arrow now, and what comes back names pattern { action }. docs/error-corpus.md carries both halves, because the finding that produced the lookahead is still the finding.
use std::<anything> is refused here and not by rustc — NK1186, which
closes #146 and leaves that file’s §1
empty.
use became a comment in the generated Rust, the call was emitted verbatim, and what the programmer read was failed to resolve: use of unresolved module or unlinked crate nosuchthing about a file they did not write — with a help telling them to cargo add a crate that does not exist. Part III C.1 says the backend must never speak about the generated file.std does not offer. use std::tools names crates/nikaia-std/src/tools/, which holds ADR-196 D2’s Rust-signature grammar and is deliberately not part of std — so the refusal a program deserved for writing it came from the wrong compiler.std’s ledger declares, which is what std has. Derived, so a module std gains is one a program may import the day it lands — no upkeep.db, json, process, thread, panic, build, task, backend. Refusing those is Part III C.4’s correct program refused — use std::db is how ADR-143’s driver is reached, and the day it exists nothing about the line changes. Written down, because there is nothing to derive it from.str::len, i64::to_string and list::ListExt::map sit beside fs::read and look the same from outside; a primitive and a trait are reached with no line at all (Part I 1.3, 2.2). Subtracted rather than listed, which is the direction that needs no upkeep. A derivation was tried and does not hold: a module’s function has a named first parameter where a method has a receiver, except that every entry of collections is a method on HashMap and collections is imported by name.use std::collection → did you mean use std::collections?, off the same edit-distance helper every other misremembered name in this compiler uses.use std::nosuchthing → the list: std offers: channel, cli, collections, foreign, fs, html, http1, io, net, text, time.use std::tools → its own sentence, and that is the case that found the defect: tools is the toolchain’s own, not std’s. Told nobody has written that down a reader would go looking for a typo.use std::db::postgres is db’s question and use std::backend::x86 is backend’s, which is the reading ADR-140 already gave them.Every entry that section ever held was found by running something — the specification’s own programs, the corpus at both settings, a two-file project, a change made twice — and never by reading the code. So an empty §1 says what has been run, not what is correct, and the way to lengthen it is that method.
@borrowed is removed, and the word that allows a tether is @tethers —
ADR-201, narrowing
ADR-008 D6’s assertion half and leaving its
inspection half standing.
@borrowed above a struct: why is this here? is it needed? is it missing somewhere else? Its whole job was to make a silent state change loud, so its presence and its absence looked identical on the page — a struct that should have carried it looked exactly like one that should not.NK2302 or NK2303 either way, and the emitter wrote a comment into the generated Rust on each of the eight structs carrying it saying the check does not exist. A decoration that reads as a decision.is_borrowed, the ledger’s borrowed column, the emitter’s comment, and the word from thirteen structs across examples/, benches/, crates/nikaia-std/src/tools/ and the fixtures. NK2302’s help no longer writes it into the shape it suggests, and the editor grammar and its scope test no longer colour it.taking_the_word_away_took_no_refusal_away builds the shape NK2303 refuses, without the word, and gets the refusal..contracts file written before this is refused — unknown key borrowed on a type — and the answer is to rebuild, because the file is derived and every build rewrites it. The version key is not raised for it: nothing reads that key to decide anything, and raising it would claim a compatibility story this project has not written.@tethers, above the struct: this struct may keep its buffer alive. Without it, a value that would outlive the buffer it points into is an error — which is what happens today, so the default is not a new refusal but the one already standing, given a reason a reader can act on.unsafe and ADR-009 D1’s for unchecked, which editors/’s own grammar already separates out as an assertion you make rather than a question you ask.the_word_that_will_allow_a_tether_is_not_built_yet is the test that fails on the day the state arrives — which is the day the word should.tokenize example changed, and it is the part of this a reader will actually hit. It said the returned tokens tether with no annotations. The annotation is the point: they tether because Token says @tethers, and a Token that does not say it is refused with the buffer and the escape named.1brc.nika’s Reading writes nothing above it, and the comment says why that is the promise: a later edit that let one escape would be refused there.--tethers is untouched and turns out to be the better half of D6. It prints the solved state of every view and changes nothing; a change of state is a ledger diff in review. That is the tool for which state is this in, and it never needed an assertion beside it.docs/stored-views.md gets a postscript rather than an edit, which is what a dated laboratory note gets: its transcripts stand as they were taken, and one sentence says which spelling in them has since left the language.The HTTP server, and nikaia serve is cut —
ADR-194 D5 built and
ADR-200 written, which together close
#90’s order down to one step.
examples/hello-http/ is a server. http::listen(at) fn(request) { … } — one address and one function that decides — and crates/nikaia/tests/project.rs drives it over a real socket: port 0, the bound address read back off the line the server prints, then GET /, GET /nowhere, a POST with a body, and each of the six refusals.examples/http/, which is Nikaia: listen accepts, answer reads a head and a body and calls the handler, refuse answers a client this will not answer properly. The socket is std’s and the protocol is the package’s (ADR-194 D1), and neither half says async, await or epoll.body_cap 1 MiB, head_cap 16 KiB, head_wait 10 reads, connections 0 for keep going. A program that wants the defaults says nothing; a test names connections.std::http1 — HTTP/1.1’s text half, and where the line runshttp1::Buffer holds the bytes as they arrive and answers three questions: head_end(cap) for where the head ends, head() for what it says, text_from(at) for the body. Plus take, len. All sync: it reaches no socket, so a server calls it between two steps that do pause and neither of them is this.Buffer is on std’s side of the line because deciding that a head has not arrived yet is reading HTTP/1.1, not reading a request. What a package holds is what a request means.0 is not yet, and not a missing value. A read loop over while ended == 0 is one condition where a loop over an absence is a condition plus the unwrapping after it — and a head cannot end at byte zero, so the two readings cannot collide. That shape was chosen after the first draft handed back a Head? and the package could not be written against it.http: a program reaches a package by that word (ADR-069 D1), and a std module of the same name would make http::Response mean two things.GET or POST, a content-length that is not a count, and a transfer-encoding at all — because a server that ignored that one would read a chunk header as a body. Nine tests in crates/nikaia-std/tests/http1.rs.Bytes::len and Bytes::is_empty are in the ledger now. net::Connection::read says empty means the peer closed, and a loop over a socket cannot be written without the question. The Rust type had both; nothing had written them down.Both were rustc about a file nobody wrote (Part III C.1), and both were found by being the first program to write ADR-102 D1’s parameter for real.
impl AsyncFn(A) -> R, an opaque type with no Copy, so a listen that passes its handler to an answer inside its accept loop handed it away the first time round: use of moved value: handler. The answer is the one Part I 5.4 C already gives — a run parameter is immediate and borrows — reached through the column every other parameter is: Ty::Fn moves, so keeps::lends claims the position where the body does not keep it. &impl AsyncFn(A) -> R in the declaration, &handler at the call, and &F is a function too, so one & is all it takes.mut parameter handed to another mut parameter gained a second &mut. refuse(&mut connection, …) for a connection that is already a &mut Connection is not a &mut &mut T that derefs: a &mut may only be taken of a binding that is itself mut, and a parameter is not one — cannot borrow connection as mutable, as it is not declared as mutable. The binding now records that it is a mut parameter (scope-correct, so a shadowing let stops being the answer), and a bare name in that position is handed straight on for the language below to reborrow. A bare name only: &mut c.field for a mut c is the reference the call wants.rustc accepting both at once.nikaia serve is cutADR-200, narrowing ADR-194 D2 and D3’s operator half.
serve is the half nothing else waits on (ADR-018, ADR-058’s body rows and the roadmap’s route hashing all wait on the application server); and its value is questionable — what a Nikaia file server would demonstrate about this language is the part it has least of, with no handler, no routing, no ledger and no provenance.--trust’s listing of a wide bind, which it put in ADR-108’s shape rather than its own.NK1142 refuses a function type in a field, so nothing can keep a handler per path and a .route(…) chain has nowhere to put what it was handed. The 0.0.152 measurement that said what an application writes needs no language change ran the chain as an expression and never stored one.pub. The chain is what #181’s kept lowering buys.the_corpus_has_no_more_unanswered_method_calls_than_it_had: 51 → 62, and eleven have one cause with one exception. A binding whose value came through a catch has no type in this compiler — the gap Part I 6.5’s own status block already names for the & it cannot write — and four of the five lets in the server’s answer are … catch { … }, so ten calls on receivers the program states plainly go unread. Measured piece by piece: let b = http1::Buffer() answers every call on it, and the same Buffer reached through a catch answers none. The eleventh is the sweep’s shape and not a gap: a lambda parameter is typed from the function type it fits (measured with the struct in the same file), and what this one cannot see is http::Request, because the sweep reads one file at a time.the_lengths_are_i64_and_are_all_called_len: six names to eight. Bytes::len and http1::Buffer::len, both i64, both called len. That list is what makes adding one a decision rather than a line.touches? ADR-194 §4 deferred it with the owner has asked to be asked again, with a fuller write-up, when the work reaches it — and the work has reached it. It is on open-decisions.md now, in that page’s shape: what is blocked, three options, a recommendation (list it, do not refuse on it) and what either direction costs.A readiness wait is a registration, not a worker — ADR-199, which is #90’s second step and not what that line said it was.
io-workers is 1 by default (ADR-038 D4, one I/O thread always runs), so a wait that had not answered blocked every other wait in the process.accept while a connection waits on read is that shape exactly, so the HTTP server could not have been built on it.a_wait_that_never_answers_does_not_block_one_that_would — so it cannot come back.ADR-009 D4 asks for a number before a shape is chosen. #90 called this step keep registrations:
| per wait | |
|---|---|
| a poller built per wait, which is what the worker did | 7.4 µs |
one poller, add and delete around each wait |
2.5 µs |
| a registration kept and re-armed | 1.2 µs |
| the whole round trip through a worker | 35 µs |
rt::io::waiting, which is what a server uses, goes 35.4 → 6.6 µs. The blocking rt::io::wait goes 35.3 → 29.2 and stays a condvar round trip, which nothing hot uses.wait, and arming on the calling thread — registering is an epoll_ctl and does not block, so there is nothing to hand to anybody.Runtime::pending counts it and ADR-006 D5’s drain waits it out. The park still asks is anything outstanding before it sleeps.Waker does not reach a thread asleep in io_uring_enter or on a condvar (ADR-121 D1).Op::Readiness and poll_one are gone. The worker enum is back to what it is for: operations that do something, where blocking a thread is the work rather than the waiting.A socket in std — ADR-198, building
ADR-194 D1, and with it the step
#90 calls the blocker.
.nika program binds a socket and both ends talkuse std::net
fn main() throws {
let listener = net::listen("127.0.0.1:0")
let mut connection = listener.accept()
let asked = connection.read()
connection.write("pong")
}
listen, connect, accept, read, write, with address, peer and close beside them. read hands back ADR-156 D1’s one shared buffer, as fs::read does; write takes all of it, because the loop that would follow a partial write is inside it; and an empty read is the peer closing, which is what a zero-length read has meant on every socket there has ever been.user_parallelism, with the same output from each. That is the claim the switch rests on and the one a socket could quietly break: accept gives the thread up rather than holding it, so a program that waits for a connection and then makes one is a program that deadlocks the moment the wait is a block. At no there is one thread and it is the same answer.async, no await, no epoll, no io_uring, no poll. ADR-038 D3 held where it is hardest to hold: every other language makes a reader choose a runtime before they can bind one.untrusted (ADR-010 D2), and it is the first source in std that is. Every other one is the operator’s own — a file they named, a pipe they connected, the arguments they typed. std.contracts’ own header predicted this one by name: the untrusted ones — http, net, db — arrive with the modules that have them.touches gains socketstdout and a Locked value are not in the set of things that can be pointed at — and the kind waited under the rule lock waited under: a word waits until a program can ask for it.lock’s reason rather than for want of a name: a listener could be named by its address and a connection is what a program touches, and telling two of those apart is the alias analysis ADR-039 D4 refuses to make a program’s compilation depend on.10-nikaia-light.md #62 — the example that introduces throws — wrote net::send(file), a function that did not exist because the module did not. It now uses fs::read_to_string and net::connect, which is the same lesson (two things that can fail) written in an API that is there.tests/specification/EXPECTED.txt and COMPILES.txt both move one line.bind and connect block. Binding is a syscall that answers immediately; connecting is not, and this one blocks on it — what the MVP connects to is a listener on the same machine, and a non-blocking connect is a second readiness shape for a case nothing here has yet.Bytes costs: what comes back is a value a program may keep, so it cannot be a window into what the next read overwrites. A reusable buffer is a second function beside read the day a measurement asks for one.Cleanup is unbuilt and a socket is not the thing to build it for.The row a bound cannot answer —
ADR-193 D4’s intra-crate call graph and
use table, which is the last step of that record.
# `across_a_thread_unchecked`: reaches `tokio::spawn` through `on_one_worker`.
# A sink reached through a call says *this function threads something*,
# never *this function threads your argument* (ADR-193 D4) - connecting
# those is dataflow through a closure capture and is not built.
# Does this put it on a thread? -> threads = true | false
unsafe impl<T> Send for Smuggled<T> took the bound away, so 0.0.162’s signature scan is correctly silent about it — and the only thing between it and a tokio::spawn is on_one_worker, which is private.fn is reported rather than skipped, which is the same move a private mod got: nothing outside the crate can call it, and the graph goes through it. A reader that dropped it could not follow the calls at all.across_a_thread gets both notes, because they answer two questions: what it asks of its caller, and what it does.use tableuse tokio::spawn; spawn(x) and tokio::spawn(x) are one function and two strings. A body’s calls are resolved against the file’s use items before anything looks at them — the items are read first, because one may be written below the function that needs it.pub use is an import here too: it brings the name into the file exactly as a plain one does, and offers it onward besides.use inside a mod reaches only that block, and treating it as the file’s can only make a name resolve where it would not have — which for a note is a wrong sentence to a reviewer rather than a wrong claim in a file.(, lexically — so match (a, b) { is not a call to match, and f (x) is not read as one either, which is the price of not writing a Rust expression grammar and is worth naming.spawn::<T>(…) is a call to spawn.block and BLOCK would be one rule and the generated Rust would not compile. Written down in the file rather than met twice.nikaia lower-std emitted without checking. lower_std_module went from the parser straight to the emitter, so a module this compiler would refuse a program for lowered in silence — and rustc was left to complain about the generated file, which is Part III C.1.tools/rust.nika had a grammar action calling a function that could pause, which NK2209 exists to refuse in one sentence on the .nika line. What came out instead was error[E0728]: `await` is only allowed inside `async` functions about rust.rs:193.std is written by the people who write the compiler, which is exactly why the checks a program gets were the ones this half was going without. a_sysroot_module_is_checked_before_it_is_lowered is the test.match arm’s binding has no type here, so many.drain() resolved to nothing and took flat’s sync claim away — fail-closed, and correct as far as it goes.Send is matched as a word: a .spawn(…) method on the crate’s own type would match a bare spawn, and a note about a function that threads nothing asks a reviewer a question with no answer. A crate that imports the name is answered by the use table, which resolves it back to a path.The describer proposes and never claims — ADR-193 D3 and D5 built, which is the half of that record a person reads rather than the compiler.
unsafe impl Send for Smuggled<T>, at the top of the file, before the entries, because it is about the crate. One syntactic pattern and sound in the only sense that matters here: the item is in the text or it is not.private type, which is what every other step in D4 is blind to — Smuggled is not pub, has no signature anywhere, and is the hole examples/foreign-runtime/shim opens on purpose.Send bound is evidence, and evidence is a sentence# `across_a_thread`: the parameter `value` is bound `Send`.
# Does this put it on a thread? -> threads = true | false
# Seen and not claimed (ADR-193 D3): a `Send` bound says the callee **may**
# send it, which is usually `spawn` and is sometimes an API keeping a door open.
[fn."hyper_shim::across_a_thread"]
crosses because a field holding an Rc entails not sendable. A Send bound entails nothing: it says the callee may send, which is usually spawn and is sometimes an API keeping a door open. Written as a claim it could refuse a correct program (Part III C.4); written as a note it is the most actionable sentence the tool can produce.across_a_thread_unchecked is correctly silent, which is the finding rather than a gap: an unsafe impl Send took its bound away, and only following the calls reaches the spawn. That row is ADR-193 D4’s call graph and is not built.Send is a word. Into<Sender> and Resend + 'static are not the bound, and a note about a parameter nothing sends is worse than no note: it asks a reviewer a question with no answer.where clause (Fun.wheres). Half the bounds a Rust crate writes are there rather than in the <…> list, and a reader that dropped it would see half the evidence.Notes is how a description carries a sentence — crate-level and per-entry, rendered as comments and never parsed back. A derived ledger has none, so render() is byte-for-byte what it was.w.trim() on what a grammar pattern bound is a method call on an untyped receiver, like the ten before it.A path is relative to the file it is written in
(ADR-197) — and with the three experiments
building again, threads gets measured, and both crossings are refused by
this compiler.
nikaia.toml named a Rust crate path = "../../../../shim" — four .., because ADR-002 D1 said the value reaches Cargo verbatim and the generated manifest used to sit at target/nikaia/build/Cargo.toml. ADR-053 D1 gave every package a member directory of its own, so the manifest Cargo resolves against is one level deeper and every one of those .. is one short. All three projects under examples/foreign-runtime/ have been unbuildable since, and nothing said so: the tests that build them fetch hyper from crates.io and are #[ignore]d, and CI does not run ignored tests.path is relative to nikaia.toml, for both arms of [dependencies] — what a reader would guess, and what a Nikaia package’s path already did. D2 narrows the passthrough by exactly one key: a relative path is resolved and written absolute into the generated manifest, which that manifest already does for nikaia-std. The alternative was counted out and refused — ../../../../../shim is a number a person derives from a build layout that changed once already without anybody noticing.describe::crate_sources resolved against target/nikaia/build, so nikaia describe and cargo build looked in two different places for one crate’s sources — and only the first had a test, because describing.rs never runs Cargo.contracts/<crate>.contracts did not reach the build cache’s key. A hand edit — which ADR-104 D5 expects, because the draft is committed and reviewed like code — took effect only after the build directory was thrown away.Choices::describes is the dimension that closes it, beside Choices::reads and for its reason: the file binds the whole build rather than one unit (ADR-021 D7’s every dimension in one place).examples/foreign-runtime/ say threads = true where it is true. across_a_thread and across_a_thread_unchecked build a tokio runtime and spawn; serve_once builds a multi-threaded one. Hand-written, because nothing a signature can show entails it either way (ADR-193 D1).crossing/ is refused by NK2502, in this compiler’s vocabulary, on the author’s line. It used to be rustc’s Send bound with the place fixed and the text still Rust’s, which ADR-005 D7 carried as its open half. For this program it is not open any more.smuggled/ is refused too, and foreign-runtime.md §3.5 did not expect that. That section says a structural Send check would not have caught this one either — and the sentence is still true, because the check that catches it is not the one it is about. A structural check reads what the language below says about a type; this reads what a person wrote down about it: crosses = false, because LocalHandle holds an Rc. An unsafe impl<T> Send for Smuggled<T> cannot change that line, because the line never asked Rust.crosses = true about the same type gets exactly as far as it did before. What moved is who has to be honest — and a .contracts file is committed and reviewed like code.#[ignore]d experiment tests pass, two of them for the first time since they stopped building. docs/history/foreign-runtime.md §8 is the write-up, added rather than edited in.threads is a column — ADR-193 D1 and
D2 built, and with them the half of
ADR-038 D7 that was left open.
Crosses’ shape one question over: true, false, and absent. The absence is nobody said, never it does not — a threads = false written by a hopeful hand is a false silence, and silence read as no is the polarity ADR-010 D1 calls a vulnerability generator..nika file declares says it: a Nikaia function that wants another thread writes a task, which the compiler sees and which is not this question. threads is about a body written in another language.crosses has, because the ledger is a committed file.NK2502 asks a described call now, where the description says the word — and never on its absence, which is D2. Before this, a crate that answered every other question honestly turned the check off by being described: NK2502 asked its question of a call nothing describes, which is ADR-038 D7’s own wording, and no column said whether a described foreign function puts what it is given on a thread.f would send a reader to write a description that is already there.a_described_call_that_says_it_threads_is_asked_and_a_silent_one_is_not in crates/nikaia/tests/send.rs holds all three values: silence refuses nothing, threads = false refuses nothing, threads = true is NK2502 against the argument.hyper_shim::across_a_thread builds a tokio runtime and spawns, so threads = true is true of it and a reviewer would write it — but the test that would show what changes cannot run, because examples/foreign-runtime/crossing **does not build.path dependency is resolved against a directory the generated manifest is not in. The workspace manifest is at target/nikaia/build/Cargo.toml and the member’s is one deeper (ADR-053 D1’s one member per Nikaia package); Cargo resolves a path against the manifest it is written in, so ../../../../shim lands one directory short. Three manifests say that, and so does the comment in each.describe::crate_sources resolves against target/nikaia/build, so nikaia describe and cargo build look in two different places for one crate’s sources — and only the first is exercised, because describing.rs never runs Cargo.type removed reaches Cargo verbatim: a path in nikaia.toml should be relative to nikaia.toml, with the generated manifest carrying it absolute — which that manifest already does for nikaia-std. That wants a record rather than a commit.A module is a file as often as it is a block — the last of the scraper’s three named limits that a parser can close, ADR-195 D3’s removes the class rather than the instance finished.
src/lib.rs is the crate root, src/foo.rs and src/foo/mod.rs are foo, src/foo/bar.rs is foo::bar. src/main.rs and anything under src/bin/ are a binary’s and are skipped: a program that calls into this crate cannot reach them. The scanner read every .rs under src/ as the crate’s own, so every item in every file was fremd::<name> and none of them was where a caller writes it.pub mod shown; in lib.rs offers shown::seen, mod private; offers nothing, and a pub use private::hidden as rescued; beside it offers exactly one name. So reachability is a second pass over every file rather than a decision taken while walking one.src/loose.rs with no mod loose; anywhere is fail-closed at both paths a caller might guess — ADR-010 D1’s polarity: the absence is nobody said this is public, and the name reaches the reviewer as a ? rather than the draft as a claim (ADR-104 D4, D5).describe.rs’s own header apologised for, that is the only one still there.impl’s pub fns; what one is at a foreign boundary is ADR-104 D4’s own question and nothing asks it yet.The scanner is out, and nikaia describe reads a crate with a parser
written in Nikaia — ADR-195 D3 built, on
ADR-196 D1’s route.
crates/nikaia/src/describe.rs loses items_of, signature_at, fields_at, line_starts and matching — the hand-written character scanner its own module header apologised for — and calls nikaia_std::tools::rust::file instead. translate is untouched, because the grammar hands it the same text; split_top_level stays for the three things that are lists and not syntax (a type-parameter list, a use list, a Result’s two arguments).nikaia-std is a real dependency of the compiler (ADR-196 D3). Measured before the edge was drawn: four packages the compiler did not already resolve — nikaia-std, memmap2, polling, io-uring; rayon and libc were already in the tree — and it was a dev-dependency here already. No cycle: nikaia-std may not depend on the compiler (ADR-002 D4), and its own manifest says so.an_item_that_is_only_text_is_not_described_and_a_re_export_is in crates/nikaia/tests/describing.rs. On one crate the scanner wrote entries for four functions that do not exist:
| what it is | where | the scanner | the grammar |
|---|---|---|---|
spectre |
inside a block comment | an entry | nothing |
phantom |
second line of a string literal | an entry | nothing |
hidden |
inside a private mod |
an entry | nothing at that name |
buried |
private mod inside a pub mod |
an entry | nothing |
seen |
inside pub mod shown |
fremd::seen |
fremd::shown::seen |
rescued |
pub use private::hidden as rescued |
nothing | an entry |
rescued is a call the crate really answers, and refusing one is Part III C.4. The scanner refused every re-export there has ever been; the grammar reads pub use, keeps a private mod’s items instead of throwing the body away, and resolves the two against each other to a fixed point — because a re-export may name a re-export.the_draft_for_the_experiment_is_the_file_a_reviewer_wrote is unchanged and still passes, entry for entry, against the hand-written ledger for examples/foreign-runtime/shim. The new reader had to agree with the old one everywhere the old one was right.pub use is an item, reported with the text it was written with. Splitting a path is string work rather than parsing work, so describe.rs takes the pieces — brace lists, as, ::*, and crate/self/super heads, with Rust 2018’s uniform paths tried both ways and the first that names something taken.mod is reported and marked (Group.visible) rather than skipped. Throwing the body away would have been the cheaper answer and the wrong one: a pub use may reach through it.pub fn carries its type parameters (Fun.generics), which is what the ledger spells $T.pub item in another file is still read as the crate’s own. The half inside one file is closed — an inline pub mod gives its items a path, an inline private mod stops offering them — and the other half needs the mod foo; in a parent to say what module a file is, which is a second pass. #124 step 3.3 carries it.The grammar moves into the toolchain, and the route is running — ADR-196 D1, no longer a decision but a thing a Rust program calls.
examples/rust-signatures.nika → crates/nikaia-std/src/tools/rust.nika, lowered by nikaia lower-std to the rust.rs beside it, committed, and include!d as nikaia_std::tools::rust. crates/nikaia-std/tests/rust_signatures.rs drives it from Rust: five tests, no process, no C ABI, and &str views into the text that was handed in — asserted, because that is what ref String in the source is for.examples/ would be a second 250-line grammar to keep in step with the first, and the demonstration it was making is made better by the compiler using the thing.std, and a gate is what taught that the directory has to say so. the_shipped_std_ledger_agrees_with_its_nikaia_sources requires std.contracts to carry every pub thing a .nika beside lib.rs declares — which is exactly right, and exactly what this file must not do. So src/tools/ is a place with a rule: the release step lowers it, std.contracts may not name it, and a_toolchain_module_is_not_part_of_std is the new test that holds the line. Sysroot::tool_modules is the other half.nikaia-std names winnow directly for the first time: a grammar in this crate expands to code that writes winnow::…. A generated program has always declared it; until std held a grammar of its own, this crate reached it only through winnow-grammar.pub use is an item, and the reason is C.4pub fn inside a private mod is reachable after all when a pub use says so. A reader that dropped private modules without looking for one would refuse a call that is correct, which is Part III C.4. So pub use …; is read and reported with the text it was written with — splitting a path is string work rather than parsing work, and whoever needs the pieces takes them.use is not reported, and pub(crate) use is refused by the pub rule itself: the word has to be a word, and a ( after it is not one.clippy reads generated code too. match f { A(x) => …, else => {} } on a two-variant enum is “you seem to be trying to use match for destructuring a single pattern” — a complaint about a file nobody wrote, Part III C.1’s class whoever makes it. Both variants are named now, which is what an exhaustive match is for anyway.cfg naming a feature no manifest declares is the same class. The grammar! expansion checks trace, so nikaia-std declares it as the compiler already does.pub struct whose fields are not written pub is unreadable from another crate, and a pub enum that is not written pub makes a public field “more private than the item”. Both are Nikaia saying exactly what it meant; the source says pub now.use std::<anything> is accepted, and rustc is the one that says otherwise — #146, the first entry that section has held since 0.0.137. use std::nosuchthing lowers, the use becomes a comment, the call is emitted verbatim, and what the programmer sees is error[E0433]: … unresolved module or unlinked crate about a file they did not write, with a help telling them to cargo add a crate that does not exist. That is Part III C.1.use std::rust is refused. It is not. The checker already has the list it needs — what a program may write after use std:: is exactly what std.contracts declares — so what is missing is the refusal and its code.The grammar, and the route it travels — ADR-195 §5 step 1 written, and ADR-196 answering a question that record left open: how does a Nikaia-written piece reach the Rust program that calls it?
examples/rust-signatures.nika — a Rust file’s public surface, read by a Nikaia grammarpub fn (with async, const, unsafe, extern "C"), pub struct, pub enum, pub trait, pub mod and impl headers, the type text under them, and the module and impl path each item was found at. Compiled and run at both settings of user_parallelism by crates/nikaia/tests/examples.rs.nikaia describe reports four functions that do not exist: spectre (inside a block comment), phantom (the second line of a string literal), hidden and buried (both inside a private mod) — and puts seen at the crate root rather than under shown. The grammar reports none of them, and puts seen where it is.JUNK crosses a string, a character literal, a comment and a balanced brace group as single units, so a private mod’s whole body is one skip. The scanner matches pub fn at the start of a line and counts braces without knowing what a brace is inside.impl reports its pub fns; an impl Trait for Type reports all of them, because a trait method is as public as its trait. Two rules, where a scanner has one.unsafe impl Send for Smuggled<T>, which is ADR-193 D4’s own flag, arriving free with the parser.CHAR requires the closing quote, so Box<dyn Any + 'static> falls through to one character at a time instead of swallowing everything to the next '. That one is written down in the file, because getting it wrong is silent.syn too — a raw string, a nested block comment, pub use, and a tuple struct’s fields.crates/nikaia-std/src/text.nika has taken the third since ADR-014: lowered ahead of time, committed, include!d, and reached with a plain call and plain types.unsafe, no second binary to build, find and install — and no interface to design, because a boundary whose types are Rust’s own is one nothing has to agree about.describe’s parser, and ADR-038 D6’s HTTP/1.1 parser — ADR-194 D5’s Rust until it is not, with the until now having a shape. And the second half of the owner’s point is kept: the lowered form is an ordinary Cargo module, so a thing written in Nikaia is available to Rust programs that have never heard of Nikaia. One implementation, two ecosystems.crates/nikaia’s nikaia-std dev-dependency becomes a real one; nikaia-std may not depend on the compiler (ADR-002 D4), which is already written in its own module header.fs has a directory walk and the Rust half is still doing the walking.no_nika_file_says_async_or_names_a_mechanism (ADR-038 D3) forbids the word async in any .nika file outside a comment. A grammar over Rust has to name Rust’s keyword. The gate is narrowed to strip a string literal’s body as well as a comment — what it is about is what a program does, and a literal is data it handles — while keeping the inside of an f"…" interpolation, which is ordinary code. a_literals_body_is_dropped_and_an_interpolations_inside_is_not is the test that the narrowing did not hand the gate away.pauses, which is what the ledger records (ADR-104 D3) and the only word this language has for it.the_corpus_has_no_more_unanswered_method_calls_than_it_had rose from 39 to 50, and every one of the twelve is in the new file. Ten are the ceiling’s own sentence — a method call on what a grammar pattern bound is unanswerable by construction. Two are a finding: f.parts.drain() and r.parts.drain() in show, where f is bound by a match arm Item::Fun(f) and the variant’s payload type is written down. Checker::pattern_bindings gives every pattern binding Ty::Unknown without consulting the enum. Typing those takes the number down; it is not done here because giving a binding a type where it had none can refuse a program that compiles, which is Part III C.4’s own piece of work with its own sweep..nika in the compiler’s own tree, the release step that lowers it, the Cargo edge, and the scanner coming out are step 3.2 of #124.line.push_str(f"…") does not compile — an interpolated string is a String and push_str wants a view. The example builds its lines with f"{line}…" instead. Not filed as a defect yet; it wants a decision about whether a String argument may be read as a view at a call, which is a ledger question and not a bug.nikaia describe is a Nikaia program —
ADR-195, the owner’s answer, and the reason
is not the one 0.0.154 led with. No code changes: the ruling and the order.
Rc against Arc, a match against a perfect hash, a boxed future against impl AsyncFn. It does not reach a choice already made on a reason about the project.describe is the first tool that can be written in Nikaia without anything else moving..nika kept beside the .rs, pre-lowered at release, so a binary install needs no compiler. nikaia lower-std already does this for std.syn step and nothing else in it. All three of the scraper’s named limits come from its being a scanner; the one that survives a real parser — a macro-generated item — survives syn too, and for a reason that is neither parser’s.std gains a directory walk and a subprocess, both the kind of operating-system resource ADR-194 D1 just put the socket in std for.describe is the right first onesplit_top_level, items_of, fields_at, line_starts, signature_at, matching — is the part that has been wrong three named ways.Two things are called a parser, and only one of them was decided — the owner’s differentiation, asked properly. No code changes.
| the HTTP/1.1 parser | describe’s Rust-signature parser |
|
|---|---|---|
| runs | per connection, in a server’s hot path | once per crate, when a person types a command |
| the compiler needs it | to serve | never — NK2504 names the command and a person runs it |
| decided | Rust for the MVP (ADR-194 D5) | open, and now asked |
describe is a good self-hosting candidate, measuredstd’s Nikaia half — the .nika kept beside the .rs — so a binary install compiles the .rs and needs no compiler to do it, and nikaia lower-std is the release step. A Nikaia describe rides the same one. There is no chicken and egg.describe.rs is 843 lines, and split_top_level, items_of, fields_at, line_starts, signature_at and matching are a scanner written by hand — which is what the module header already apologises for, a signature scraper and not a Rust parser.std’s actual surfacestd has fs::map, fs::read, fs::read_to_string, fs::write and cli::args — and no directory walk (reading a crate means reading its .rs files) and no subprocess (ADR-193 D4’s first step wants cargo metadata).std for, so ADR-069 D2’s subtraction covers them: work, not a boundary problem — but two pieces of new std surface this question would be the reason for.describe’s three named limits, and a grammar removes the class rather than the instance. That is the move ADR-117 D2 made with a reserved list over a cut, and ADR-048 D1 with a trait over an emitter case.syn would remove it too, in a tree that is already Rust — that is B’s real strength and it is not waved away.examples/foreign-runtime/ already has, compared with the scraper’s own output. An afternoon, and it settles the question with a measurement instead of a preference — the route rc-or-arc.md and lock-free.md both took.describe is security-adjacent — which is ADR-193 D3’s whole asymmetry. That its output is reviewed by a person is what makes the risk bearable, and it is why this is a better first self-hosted tool than one whose output nobody reads.Two servers, not one — ADR-194, the owner’s answer, and it dissolved a conflict this session had built. No code changes: the ruling and the order.
nikaia serve [dir] — has no author present at run time, so everything it does is the operator’s: the directory, the port, the limits, whether it leaves localhost.std’s, the protocol is the package’s. ADR-069 D2’s subtraction carried one item further: that record itemised what needs the compiler and pushed the protocol and the framework out, and a socket is neither — it is an OS resource of exactly the kind fs and io already own, and rt::io’s own words are a socket is not a file; epoll answers a socket exactly. The http package is Nikaia and cannot syscall.nikaia --trust — is where such a thing belongs..route(…), no @route, and never every pub fn: pub is a package word (ADR-047 D2), and reading it as the network may call it would give one word two meanings and make the dangerous one invisible at the declaration.GET and POST, bodies by Content-Length, Connection: close, no chunked and no TLS, with a body cap and a connection cap from the first commit — a server without them is a denial of service rather than a default. Moving the parser into a Nikaia grammar is step 4 of ADR-038 §4.5: self-hosting the protocol is a step deferred, not a step forgotten.touches? The ledger knows, and no other language’s http.server has that column. The owner has asked to be asked again, with a fuller write-up than the one sentence it has, when the work reaches it. ADR-194 §4 carries it and open-decisions.md says why it is not in that page’s shape yet: putting a question in answerable form before there is a server to refuse anything is the kind of premature entry that page is worse for holding.open-decisions.md is empty againthreads column → ADR-193, and the microservice shape → ADR-194.A package’s type could not be constructed through the package that declares it — a defect found by writing ADR-018’s own shape, fixed, and the whole chain now runs across a package boundary. The owner’s two MVP decisions are recorded with it.
tiny::Server() lowered verbatim. The constructor rule
(ADR-140 D2, Type() is Type::new()) asks the library’s ledger about a qualified name and never the package’s — so a bare Server() in one file became Server::new() and tiny::Server() went out as it was written.rustc answered about a file nobody wrote: use struct literal syntax instead and type annotations needed (Part III C.1).NK1149’s own help hands over — write tiny::Server(…) — so the way out could not be taken either, which is C.2’s rule that a way out that cannot be taken is not one.use tiny as t writes t::Server and the ledger keys tiny::Server::new.crates/nikaia/tests/project.rs runs ADR-018’s chain across two packages — four things at once, because the point is that they compose:
handler: fn(tiny::Request) -> tiny::Response;async closure ADR-192 D1 writes for a run parameter, handed to a callee in another package, lowering to impl AsyncFn(Request) -> Response.The socket is the only thing missing.
http::Server::new(), and NK1149 answers a type is constructed by its anonymous constructor since ADR-140 D2. The record is not rewritten; whoever builds the server writes http::Server().self, ref self and ref mut self are all receiver forms, so a consuming fn route(self, …) -> Server chains exactly as the record prints it. mut self is not one — that is ref mut self..route(…) for the MVP, no @route yet. The exposure decision is in the source, where nothing is implicit — the safest of the four options and no language change at all.The server’s runtime blocker has been built since ADR-121, and the microservice question is asked — the owner’s HTTP from the command line, like Python’s. No code changes.
rt::io::wait — the readiness half this would rest on — cannot be awaited, only blocked on … nothing here can be an async fn that actually pauses until it is answered, and pointed at a question open-decisions.md has never held.eventfd on the ring so a worker’s reply completes a ring job and the park returns; D4 says the same is true of rt::io::wait, which is what the server’s socket layer awaits; and its §3 names this entry’s own need outright: rt::io::wait is awaitable, which is the first thing the HTTP server’s socket layer needs. rt::io::waiting is the future beside it.Response as an ordinary value, and the request’s strings as views.fn route(path: ref String, handler: fn(Request) -> Response) parses and lowers (ADR-102 D1, ADR-192 D1 for its shape).pub fn’s signature, throws, sync and touches — which is what makes the tempting answer tempting.Who decides what is on the network? Four options, and the recommendation is D with C kept, and never A:
pub fn is a route. Recommended against. pub is a package word (ADR-047 D2) — a consumer of this package may call it. Reading it as the network may call it gives one word two meanings and makes the dangerous one invisible at the declaration. python -m http.server is already a famous footgun for serving the working directory; this would be the same mistake over a call graph.@route("/total") pub fn total(…): explicit and next to what it exposes, the shape @borrowed and @frame have. Kept for the suggestion, not for the grant.nikaia-runtime.toml.Why D. ADR-038 D5 moved cleanup-deadline out of the manifest for exactly this reason: how long a program waits at exit is an operating property, and a build-time key cannot be tuned by the operator — who is not the person who compiled it. What is exposed to a network is an operating property in that same sense, and more sharply: the person who compiled a function and the person who decides it may be reached from outside are routinely not the same, and only the second knows what the network is.
http.server has that column.0.0.0.0 is half of why Python’s is a footgun. Localhost unless told otherwise is the fail-closed default, and it costs one flag to leave.Citing a question is not asking it — the list’s own cross-references followed, and four of them did not resolve. Found while answering is the tether next?; no code changes.
rt::io::wait cannot be awaited … That is the entry in open-decisions.md. That page has held no entry about rt::io::wait in its whole history — checked against every heading it has ever carried. So the first item in the list rests on a question nobody put in the shape that page asks for.open-decisions.md. Never there either.open-decisions.md about a pausing code parameter’s shape — answered by ADR-192 D1 at 0.0.144, six versions ago.open-decisions.md’s question about a described foreign function and a thread — answered by ADR-193 at 0.0.149, one version ago.fn apply(f: fn() -> String) -> String { return f() } parses and lowers. The entry said it was a parse error, which it was when the entry was written (ADR-102 D1 opened it, ADR-192 D1 decides its shape).fn tell[T: greet::Speaks](x: T) is a parse error at the :, which is the bound-takes-a-path entry and waits on a module question nobody has decided.route itself rather than the language.examples/, benches/ and tests/ solves to Borrowed, and the entry says outright that the piece that was going to decide whether the rest is worth starting has answered: nothing in the tree needs Tethered. It is also the most expensive thing in the file — at least four change packages, of which the representation is the expensive half.lock-free.md §6’s the question is not how much would it save but does a real program need it, and staging-candidates.md’s without a measured crossover the complexity is certain and the gain is not.A description says whether it threads, and the describer proposes the answer
— ADR-193, the owner’s A with D. The
question leaves open-decisions.md and the work
arrives in #124. No code changes: this is
the ruling and the task, not the build.
threads column, hand-written like crosses, with three values. The absence is nobody said, never it does not: a threads = false written by a hopeful hand is a false silence, which is ADR-010 D1’s vulnerability generator.NK2502 asks a described call too, and fires on the claim and never on its absence. So ADR-005 D7’s second half closes only where somebody wrote the word, and the position and the translation stay exactly where they are everywhere else.true; it must never propose false. Nothing a signature can show entails does not thread — a function may spawn something it built itself — and ADR-123 D2’s licence to fill crosses is soundness, which a Send + 'static bound does not have: it says the callee may send, which is usually spawn and is sometimes an API keeping a door open.examples/foreign-runtime/shim’s two rows. In safe Rust the bound is forced by the type system and surfaces in the signature, so the scan carries that half alone; the intra-crate call graph is for the row where an unsafe impl Send took the bound away. A sink reached through a call says this function threads something, never this function threads your argument.unsafe impl Send and unsafe impl Sync: one syntactic pattern, sound, and the most useful sentence the tool can write about a foreign crate. What none of it can do is see whether the promise is true — the line between a rule the toolchain enforces and a rule it inherits.NK2502 reading it;syn and cargo metadata under it, which makes step 2 reliable rather than lucky;unsafe impl Send/Sync flagged — cheap, sound, independent of 3;use table, for the unchecked row.open-decisions.md has nothing open. It says so as a statement about what has been asked rather than about what is settled: every entry it has held was put there because something was blocked and somebody noticed, so an empty page means nothing written down is blocked.open-decisions.md’s Bytes as resting on the tether; that question was answered by ADR-179 at 0.0.127 and has not been in the file since — the third citation in that one sentence to turn out not to rest on the tether, after the grammar entry’s keeps at 0.0.137 and the third of ?. at 0.0.141.Step 2 said the right thing about two of three limits and named them in the wrong order — a correction to 0.0.147, and a better reason for the one that stays. No code changes.
?; a pub item inside a mod block is read as the crate’s own.pub item inside a mod block (that is the third) and narrows the third (that is the second). The judgement was right and the numbering was not, which in an entry that cites them by position is a defect rather than a typo.synrustc -Zunpretty=expanded, and measured on this toolchain: the option Z is only accepted on the nightly compiler. So macro-generated items sit behind the same ADR-001 D1 wall that keeps rustdoc-JSON out — one decision, two consequences, and no choice of parser moves either.derive generates impls and #[tokio::main] rewrites a body, neither of which is the pub fn signature a description is made of. A declarative macro that generates API surface is the case that bites.where clauses and paths a line scraper gives up on, which is also what step 4 needs in order to see a bound at all — and the mod-path limit is closed outright, for the reason the header already gives: the module path a caller writes is a thing only a real parser knows.Option D gets its pipeline, checked against the repository’s own worked
example — the owner’s five steps, read against
examples/foreign-runtime/shim, which was
written to study this exact hole and has both shapes in it side by side. No
code changes.
across_a_thread<T: Describe + Send + 'static> → on_one_worker(move || value.describe())
across_a_thread_unchecked<T: Describe + 'static> → on_one_worker(move || smuggled.describe())
// the bound is gone: `unsafe impl Send for Smuggled<T>`
on_one_worker<F: FnOnce() -> String + Send + 'static> → tokio::spawn(…)
Send bound is not a heuristic at all, and this repository already said so in the shim’s own doc comment: every safe way of reaching another thread carries it — std::thread::spawn, tokio::spawn, rayon’s scopes — so a foreign crate that takes a value across a thread boundary in safe Rust demands it of its caller too. Rust’s type system does the propagation and the answer surfaces in the signature. That is stronger ground than 99 %.across_a_thread_unchecked has no bound — an unsafe impl Send on a wrapper took it away — so only following the calls reaches tokio::spawn. That is the shape the shim says nothing anywhere complains about.syn instead of the scraper — yes, and the rest rests on it. ADR-104 D4 said the crate’s sources are parsed without naming a parser, so nothing is contradicted. It closes the second of the scraper’s three named limits outright and narrows the third. It does not close the first: syn does not expand macros either, so an item a macro generates is not in the text stays true and has to keep being said.unsafe impl Send row above and not for the safe one.use table, or use tokio::spawn; spawn(x) is a different string from tokio::spawn(x); and that a sink reached through a call says this function threads something, never this function threads your argument. Connecting those is dataflow through a wrapper and a closure capture — a third tool. So the output is a note that names the path, not a claim.staging-candidates.md’s a staging decision enters the compiler only together with a measured crossover; without one the complexity is certain and the gain is not. nikaia describe is on-demand, and the committed .contracts file is already the artifact that keeps the answer (ADR-100). A second cache with a key of its own is the shape ADR-021 D5 and D9 were written about.unsafe impl Send and unsafe impl Sync in the described crate. One syntactic pattern, sound — the item is in the text or it is not — and the single most useful sentence this tool could write about a foreign crate: this crate makes a promise the toolchain cannot check. The shim is the worked example, and the hole it opens on purpose is invisible to every other step.The threads question gains an option that helps the describer instead of
interrogating them — the owner’s, and it caught a false premise in the
question’s own option C. No code changes.
nikaia describe reads rustdoc JSON, which carries signatures and not bodies. It does not. ADR-104 D4 makes rustdoc-JSON the future better half and says so outright — a stable-only toolchain (ADR-001 D1) is not given up for it, so the source parser is what runs today. The command reads the crate’s own .rs text.spawn_worker: the parameter f is bound Send + 'static; does this put it on a thread? — and the person writes threads, or does not.nikaia describe already writes crosses = false for a type whose fields hold an Rc or a raw pointer, true where every field is sendable, and nothing where it cannot tell.Rc entails not sendable — a structural read, not a guess. The indicators here do not entail, so they may be notes and never claims.F: FnOnce() + Send + 'static on a parameter is a very good sign and not a fact: the bound says the callee may send it, which is usually spawn and is sometimes an API keeping a door open. As a claim it can refuse a correct program (C.4); as a note it is the most actionable sentence the tool could produce.std::thread::spawn, tokio::spawn, Sender::send in a body cost more to see: the text is in the file the scraper already opens, but knowing which function’s body a line sits in is brace tracking — more than a signature scraper does, less than a Rust parser. A step, not a rewrite.threads = false, which is the dangerous direction — a false silence, ADR-010 D1’s vulnerability generator — and a signature scraper cannot establish leafness anyway. Nothing a signature can show entails false, because a function may spawn something it built itself.true, must propose false never, and stays silent where it cannot see — the same absence is nobody said the column already rests on.Send + 'static bound on an API that never spawns, read by someone who then stops reading the notes. That is a reason to keep the note specific (name the parameter and the bound, never this might thread), not a reason to skip it: the alternative is that nobody looks.An idea recorded, and five notes that were not in the index — no code changes.
unique-ownership.md: a door with no lock in ituser_parallelism = "yes", and the doors over it could lower to plain moves.Rc against Arc per value, from an analysis that proves a reachability property — one written type, two machine representations, chosen by a proof. This is that sentence with never crosses weakened to never reachable from two at once, and the prize one step larger. And lock-free.md §6 found by looking that the pattern the language steers towards already is unique ownership: examples/1brc.nika shares nothing at all.lock-free.md §6 already set — not how much would it save but does a real program share a mutable value across threads? No .nika file here uses spawn, Locked, Shared or SharedMut. What would kill it is finding the claim cannot be made entry-shaped and sound at once, which is found by trying to state the rule rather than by measuring.open-decisions.md is for questions that are answerable, with options and a recommendation, and this one has neither yet.lock-free.md, fixed-map-lookup.md, zero-copy-send.md and foreign-runtime.md were not listed in docs/README.md §3 — four measured notes a reader could only find by listing the directory. Found while looking for where the new one belongs, which is the second time this week that reading a page against the tree turned up more than the thing being looked for.The body’s run-or-kept answer decides a code parameter’s shape —
ADR-192, the owner’s option A, closing
open-decisions.md’s question and narrowing
ADR-122 D1.
async closure. ADR-187 measured that ground and found it false.Shared[T] is Rc or Arc and which one is decided per value (ADR-037 D7); ADR-008 D2 is titled solved per construction site, not per type; Part I 5.4 C says it about this very construct — the context of such a parameter is inferred, not written … there is no @detached to write; and ADR-094 D1 already lets the callee’s body decide the caller’s &.impl AsyncFn(A) -> R. Kept — stored, handed back, given to a task: the boxed closure, unchanged, because AsyncFn is a bound and a value a field holds needs a type. sync is untouched and still writes the plain closure.async |a| { … } at a run parameter, |a| Box::pin(async move { … }) at a kept one. No move on the run shape, which is Part I 5.4 A — a lambda handed to a parameter the body only calls borrows what it captures, because the call is over before the caller’s frame is.benches/handler, control tying: plain closure 0.27 ns/call, boxed future 11.69, async closure 1.28. The shape a run parameter used to pay costs 8.7× the one it needed.keeps, which is where ADR-102 D3 already put it: the same analysis that decides whether a value is a view or kept, asked of a parameter that is code … the ledger records which of the two a parameter is. It does — fn runs(f: fn() -> String) { return f() } carries no keeps, and fn holds(f: fn() -> String) { spawn fn { f() } } carries keeps = ["f"].rustc’s words about a file nobody wrote. A callee this build has no contract for reads as run at both ends; the direction matters less than the agreement. It is ADR-094 D1’s own rule for the &: one answer read twice.keeps change is a nikaia.contracts diff in review, a tether state is one (ADR-008 D6 — the inverse tool is inspection, not assertion), and --locked fails a build whose contracts moved unrecorded. This is its third use, not a new mechanism.std’s own entries are untouched (ADR-122 D3): they are Rust with hand-written signatures, and declared_here still gates the whole question.let are NK1142 until ADR-102 D5 lands, so spawn is the one position that can keep a code parameter today — and a test holds that branch honest until then.A decision written at the wrong altitude — the owner’s critique of
open-decisions.md’s
ADR-122 entry, checked against the tree and
upheld. No code changes; the question does.
impl AsyncFn exists?, which reads like a bug report for the Stage-0 transpiler. The choice of Rust shape is downstream. What is actually being decided is whether one written type may take two representations, chosen by an analysis of the callee’s body.Three places where this language already answers yes, read off the tree rather than argued:
Shared[T] is Rc<T> or Arc<T>, and which one is decided per value — the emitter’s own words, citing ADR-037 D7. Not per build: two values of one written type in one program get two representations.@detached to write.And a fourth, built and shipped: ADR-094 D1 — the callee’s body decides whether the caller’s argument gains a &, and the caller writes nothing. The body decides the caller’s lowering is not a rule this change would break; it is one the language already keeps.
fn() -> String becomes impl Fn() -> Pin<Box<dyn Future<…>>> and fn() -> String sync becomes impl Fn() -> String. The distinction that exists is declared. So the change asks the existing inference to decide one thing more, not to be believed for the first time.keeps change is a nikaia.contracts diff in review, a tether state is one (ADR-008 D6 — the inverse tool is inspection, not assertion), and --locked fails a build whose contracts moved unrecorded.Shared[T] is decided per value is what makes it consistent; the ledger is what answers the objection that decided it the other way in the first place.?? joins two views, and a ?. takes one of a binding and not of a
temporary — ADR-191, answering
open-decisions.md’s question with the owner’s
option A and closing all but one shape of
ADR-113.
;. let name = find(1)?.name ?? "nobody" is rustc’s temporary value dropped while borrowed about a file nobody wrote — the very class ADR-113 exists to close, reopened one shape over. Measured before it was written, not after.emit::is_a_place asks whether an expression can be written to, which a ?. reach can: find(1)?.home?.city passes it at every link and roots in a call.check::roots_in_a_binding follows the chain down — a name, a field of one, an index of one, a ? or a cast over one — and answers on the root. Rooted in a binding: the member comes out as a view of it, Borrowed, which is ADR-008 D2’s free case. Rooted in a call: unchanged, the reach takes the value.user is usable on the line after user?.name — a temporary has no line after and no name to be usable by.String is &str (ADR-184 D2) and a view of anything else is &T, with .as_deref() and .as_ref() where the reach flattens. The checker says which, the way ADR-028 hands over every answer this emitter has no types for.NK1185: a fallback that owns what the left side viewsrustc’s expected String, found &str.?? "…" reads the same and costs nothing — and a test holds the refusal together with the way out being accepted, which is the half that makes it one..to_owned() and .to_string() are read by name, the same fact contracts::tether::makes_a_buffer already reads that way: no ledger describes either, and the one spelling this whole question is about — ?? "nobody".to_owned() — would otherwise type as ? and walk past the refusal into rustc’s words.u.home?.city ?? "nowhere".to_string() becomes ?? "nowhere", in crates/nikaia/tests/nullable.rs. Nothing in examples/ or benches/ writes the shape — the corpus’ one ?. reaches an i64. The .to_string() was only ever matching a left side that used to be owned.NK1185 is free today, which is the former backlog file §2’s own rule: before this record nothing produced a view on the left of a ??, so no program could be written that it would reject.rustc’s own help is consider using a let binding to create a longer lived value, and this emitter can write one — it is ADR-185 D2’s trick one construct over. Not free: a ??’s right side is lazy, so hoisting a receiver out of a ?? find(1)?.name would run find(1) where today it does not.The tether is needed at an escape and nowhere else — ?. has no escape in
it — ADR-190, correcting the reason
ADR-189 D3 gave one version earlier.
?. behind #196, the largest entry in that file. The owner asked whether the tether is not only needed at very specific boundaries.@detached lambda, written into a longer-lived container or a global. A struct field is not an escape by itself, which is the sentence that makes the 1BRC inner loop free.?. is none of those.| shape | what happens | state |
|---|---|---|
| the receiver is a local, the view stays in its scope | compiles, runs | Borrowed |
| the receiver is a temporary, the view is consumed in the same statement | compiles, runs | Borrowed |
view ?? view — user?.name ?? "nobody", and a literal is a view |
compiles, runs, zero cost | Borrowed |
the view is bound past a temporary — let n = find(1)?.name |
temporary value dropped while borrowed | an escape |
NK2303’s own case — a view of a buffer that does not outlive it, refused in this language’s words and naming .to_owned() (ADR-156 D4). This compiler already builds that answer twice.??user?.name ?? "nobody".to_owned() — cannot be joined. An owned result needs a copy on the borrowed branch, which ADR-008 D5 bans outright; a view result needs the fallback to be one, which "nobody" already is and "nobody".to_owned() is not; refusing the mixed form names .to_owned(), which the program already wrote.open-decisions.md with three options and a recommendation — A, ?? joins two views and the mixed form is refused, whose migration is six lines in one test file and none in the corpus.find(1)?.name ?? "nobody".to_owned() would refuse a program that compiles and runs today. What is replaced is the cause written beside it, in the living sentences and in one row of the index’s superseded and narrowed table (ADR-187 D4’s rule, met a second time inside one day).keeps at 0.0.137, and this at 0.0.141. The entry now carries the distinction that keeps it from growing wrong a third time.?. lends its receiver where that needs no representation, and the third case
waits on the tether — ADR-189, building
two thirds of ADR-113 and narrowing
#192 to what is left.
let name = user?.id then user?.id again was rustc’s use of moved value, with a help: consider calling .as_ref() and a help: you can clone the value beside it — two hints naming things this language does not have (ADR-008 D5 writes a copy and never inserts one), about a file nobody wrote (Part III C.1).?. takes nothing has been the language’s rule since ADR-113; the compiler did not do it.user?.name a ref String?, and user?.name ?? "nobody".to_owned() then has a view on one side of the ?? and an owned value on the other — no fit, and no way to make one that does not insert the copy D5 forbids. The receiver is usually a temporary besides, so the view dangles, which is the refusal the tether analysis would own.x.as_ref().map(|it| it.a), with and_then where ADR-052 D6 flattens.match takes x.as_ref() as its scrutinee. This half needs no representation at all, which is why it lands whole: what comes out of a reached method is the call’s own result and not a view of the receiver. Asked of the mutates column and only where every candidate for the name agrees, which is NK1138’s rule one construct over.?. over a member that is not copied still takes its receiver, and ADR-052 D8’s translated note stays for it alone — narrowed to ?. over a member that is not copied takes the value it reaches through, because a message explaining a move that no longer happens would be worse than the backend’s.NK2302, NK2303). Those refuse programs the language below already refused. This one would refuse find(1)?.name ?? "nobody".to_owned(), which compiles and runs today.keeps column of a grammar’s entry, in §1 — closed at 0.0.137 (ADR-186) without the tether, because a parse keeps its input by its declared result type and not by a state. The entry now says so, which is the file’s own rule: read against the code before it is followed.artifact key and no #[no_mangle] emitter to prefix, and the cleanup point the ledger should narrate needs NK2401, which is not built.The escape set is the language below’s, written down once and refused here — ADR-188, closing #197.
\ means was never written down, and what an unknown one was told was rustc’s\ and any character and keeps both, and the emitter writes a .nika literal into the generated file verbatim — so what a \ means has always been the backend’s answer, and no page of the specification said so.println("a\qb") was refused on the right line (ADR-012’s source map holds) in rustc’s vocabulary, ending for more information, visit doc.rust-lang.org/reference/tokens.html#literals. A Nikaia program sent to the Rust reference to find out what it may write, which is Part III C.2’s in the compiler’s own words not met and C.1’s class besides.build_time::one_escape is the table; decoded reads it to turn a literal into a value while the program is built, and an_escape_nothing_names reads it to say which escape a literal has that the set does not name.rustc refuses one file later: what moves is who says it.NK1184, in the checker, naming the set\n \r \t \0 \\ \' \" \xNN \u{…}, in a "…", an f"…", a '…' and a template alike. \xNN names a byte below \x80, because that is what the language below allows inside a string; a character above it is \u{…}.fail arm inside a lexical rule is not fatal in this parser — an error that got further still wins, progress before priority — so where the rest of the file parses, as it does here, the message would be discarded with the attempt. The literal reaches the checker with its body intact, which is all this question needs.\x or \u{…} was meant as a character, so the help names the form it should have had — \u{80}, \u{1F600}; anything else is a backslash meant literally, and the help writes the literal back with its backslashes doubled..nika in the tree passes, which is what says this is a translation and not a new rule. Measured first, because a naive scan of the source found a \/ that is not in the set — in a comment in examples/json/src/main.nika, about JSON’s own escapes. The check reads the literal off the AST, where a comment is not.\n, \", \r, \\, \t, \u{…} and \0. All seven are in the set.Rust has a stable async closure, and a premise under twelve sentences is
false — ADR-187, raised by the owner.
std module header, five emitter comments, three test doc comments, the roadmap, spec-promises.md and #187, the one the owner pointed at — said some form of “Rust has no stable async closure”.rustc 1.94.1, edition 2021, stable, no feature gate and no RUSTC_BOOTSTRAP: async |x: i32| -> i32 { x * 10 } with an AsyncFn bound compiles and runs, as do AsyncFnMut, AsyncFnOnce and async move |…|.benches/handler gains an async closure row, so the alternative is a measurement rather than an argument. Best of five, control tying:| row | ns/call | × |
|---|---|---|
plain closure (impl Fn) |
0.31 | 1.0 |
boxed future (impl Fn(A) -> Pin<Box<dyn Future>>) — ADR-122 D1’s shape |
11.99 | 38.3 |
async closure (impl AsyncFn(A) -> R) — the shape said not to exist |
1.37 | 4.4 |
| plain closure, twice (the control) | 0.29 | — |
overlap branch are async blocks because a body is a block and not something called: a closure around it would only be called once and take nothing away. spawn fn { … } and overlap { … } lower exactly as they did.task::interleave and the overlap/race families take futures for that same reason, and because rayon::join — the pool vehicle ADR-033 D10 names — takes synchronous closures and nothing can await inside one. A fact about rayon, not about Rust.std entry is still refused at the build, because that entry’s own Rust signature takes a synchronous closure: Iterator::map takes FnMut, and an async closure handed to it yields an iterator of futures, which is a different program. A fact about those entries, not about Rust.impl AsyncFn is a bound rather than a type, so a kept parameter still needs a box or a type parameter of its own. Taking the cheap shape for the run case splits run from kept again, which is exactly what D1 joined.open-decisions.md in that page’s shape, with the three options and a recommendation: A, split by run-or-kept, which ADR-102 D3 already infers.A parse keeps the text its record views — ADR-186, closing #140. §1 is empty: every defect that file carried is closed.
keeps_its reads an absent keeps on a present entry as keeps nothing — std.contracts’ own convention for sync, said once more for a second column — and a pub rule is a present entry (ADR-082 D1).Stock::file -> Vec[Entry] where Entry holds a ref String is the corpus’ own instance, and read, whose body is one Stock::file(data), is #140’s reproduction: it lost keeps = ["data"] the day ADR-140 D3 made the entry a call by name, and nothing put it back.keeps.rs and the note in #140 read the first where the ledger had the second.open-decisions.md’s Bytes question, which ADR-179 answered at 0.0.127 and which has not been in that file since. A stale citation in a notes page is what docs/README.md §1 calls a defect in its own right, and this one had been holding an entry open.tether::infer and keeps::infer walk Item::Fn and Item::Impl and never Item::Grammar, which is why read carries views = ["data: borrowed", "<result>: borrowed"] and Stock::file carried nothing at all.input exactly when its declared result may hold a view into it. Stock::file -> Vec[Entry] keeps the text; Calc::expr -> i64 keeps nothing. An empty column now means asked and no, where before it meant nobody derived it — and the two are written the same way, which is what made the old state an answer rather than a silence.touches walks the actions because what a parse does is in them; what it can hand back is the declared type, and the input is not a name an action can reach. So no fixpoint over the grammar: one rule, one type, one answer.views, as input: borrowed and <result>: borrowed (ADR-008 D7). The buffer is the caller’s text, which outlives the call — the sentence tether::of already reaches for a function with a view among its parameters, read off the shape of a grammar instead of off a signature.declared_in unions the borrowing types over every unit, by name, the way tether already reads an impl’s target by name.pub rules in the corpus gain both columns, and the five that hand back a number gain neither: Stock::file, Config::file, Measurements::file, Log::file, Fasta::file, Json_::value and report.nika’s Stock::file keep their text; Calc::expr and tests/errors/E3.nika’s f do not.input is typed ? and lends does not lend a type it cannot name, and read’s data is written ref String, which the declaration lends whatever the column says. The answer is in the file before a call site reads it.tethers.rs’ sweep over every .nika still asserts — a parse that views the caller’s text is Borrowed, so the state that is not built is still not reached.A view meets two more constructs that were never told — ADR-185, closing #139 and #153. One entry is left in §1, and it waits on a decision rather than on work.
for r in rows { let copy: Row = r } answered mismatched types with consider using clone here — an instruction to insert exactly the copy ADR-008 D5 says is written and never inserted.data does not live long enough, naming a local whose lifetime the source never mentions.NK1183. A for lends (ADR-094 D4), so the binding is a view and an annotation naming the element is a type the value does not have.Copy. A struct is not.copy.a reaches through it. That is what makes this C.2’s shape rather than C.1’s alone: the compiler knew both that the annotation was wrong and what to write instead, and said neither. The copy is named beside it, because sometimes it is what was meant — but it is the program’s to write.Oklet __nikaia_value = { … }?; Ok(__nikaia_value). The lowering of an entry is a block holding let _source = &*data and a stream over it; inside Ok(…?) those temporaries live to the end of the enclosing block, past the local that owns the text. A let makes the block a statement, so they drop at the ; and ahead of the local — ADR-164 D1’s ARM_VALUE trick one construct over, and rustc’s own hint: save the expression’s value in a new local variable._source, not something a program’s expression left behind. Every other tail keeps Ok(x), because a line the generated file does not need is a line a reader has to skip.examples/report/src/main.nika writes the first shape. tests/project.rs builds and runs the broken one, since the emitted text looked right and rustc is what refused it.The living pages say ref — the upkeep
ADR-184 owes, and
docs/README.md §1’s own rule: a stale note is a defect,
because a reader cannot tell a plan from a promise.
spec-promises.md’s nullable row, and examples/README.md.stored-views.md, nightly-cost.md, from-for-throws-and-touches.md, staging-candidates.md, handoff.md and language-review.md each carry a date and say what was true then — a snapshot rewritten to today’s spelling would be a measurement nobody took.&str that names the generated file stays. The roadmap’s const BANNER: &str = "nikaia 0.1"; and spec-promises.md’s Option<&str> are Rust, which is D2’s own line: the compiler’s word and the language’s are allowed to differ, and only one of them is the language.Array[T] is an array of any length, and & leaves the language —
ADR-184 D3 and D4, finishing the record
0.0.133 began.
Array[T] is defined and not forbiddenfn total(xs: Array[i64]) takes an array of three and an array of two; the language below has the same shape and the same name for it, a const parameter, so the function is written once and monomorphised per length. Two arrays in one signature are two lengths.ref the pointer carries it. ref Array[T] is a view of a run — Rust’s own &[T] — and needs no length in the type at all. That is not a second rule: owning needs a size and viewing does not, which is ADR-107’s own arrangement for text one type over, where the compiler picks the state per use.NK1182, because there is no call to ask and the two readings left are both something else: a struct generic over its length makes two Sections of different lengths two types, which is exactly not what a parser produces (ADR-179’s own motivating case), and a result’s length would be bound by nothing. The message names three ways out, because which was meant is not something this compiler can know.& leaves, and the bracket form with itref Array[T], in all four writers that had the bracket form — the type printer, the emitter, the C boundary and the call that makes the address.& gets a way out. The grammar’s own found unexpected token & is true and helps nobody, so the parse error carries a note naming the word and the three shapes it writes (Part III C.2)..contracts file is a machine format this compiler reads, not a program a person writes. &str, &[T] and &mut T all parse back — as the types their new spellings name — so a stale checkout keeps building.name: &String. views.rs writes the AST’s type itself — a third writer — because what a message quotes back has to be what the program wrote, arguments and all. Two doors were not enough; the test that found it is NK2302’s, which quotes the parameter back at its author.& left the grammar, and getting them back was four rounds of a script that could not tell a Nikaia fixture from an expected Rust string — both are string literals in the same file, and rewriting the second breaks a test that was right. What finally worked was not a cleverer heuristic but the compiler: every parse error names the line, and the last twenty were fixed one at a time. A rename across a language is not a sed, and the tests are what say so.A view is spelled ref X, and text has one noun —
ADR-184 D1 and D2, the owner’s answer to
the two questions 0.0.131 put on open-decisions.md.
That page is empty of spelling questions now.
& appears 98 times in the corpus’s 2 847 lines, and 82 of them are a type spelling — 53 of those 82 are &str. Only 16 are a borrow a program writes, because ADR-094 D1 already made a parameter the body only reads a view without the word.&str, and &str is the one place this language had two nouns for one type: text is String (ADR-107), str is not a type a program may write anywhere else, and that record had to explain the gap in prose.&, argued from how often it is written. That was an argument about cost where the question was about coherence, and coherence is the right axis for a spelling.ref X is a view of an X, wherever a type may stand: ref String, ref Array[T], ref Reading, ref mut T, ref self, and ref value for the borrow an expression writes. Part I 6.5 says it once and every case follows.str stays the compiler’s word for what the language below calls a view of text — the emitter writes &str and name for name is ADR-011 D2 — and it reaches no program, no signature, no ledger and no message.String becomes the text view in both. With it at one only, the checker held ref String and ref String as two types and said so: greet takes name: ref String, and this call passes ref String — a message that prints one spelling twice and claims they differ, which is worse than the rustc sentence it replaced.ref is reserved, and the corpus is not what decided thatref over every .nika file gave zero uses, which says the word is free. It is not: ref(x) is a borrow of (x) to the grammar and a call to a function named ref to its author, in the same characters. A word that is a name in one position and an operator in the next is worse than a reserved one, because the reading nobody meant is the silent one (ADR-010 D1’s polarity).ref needed no reserving, on the argument that every position wanting a type or an expression after it makes the word safe. crates/nikaia/tests/reserved_below.rs writes each word in every position a name may stand and compiles the result; ref(x) came back as this returns ref i64, and the function declares i64. A count over the corpus cannot see a collision the corpus does not write..contracts that carried a view, and the tests that assert on a printed type. The two specification baselines moved in their preview text only: not one verdict changed, which is what a pure spelling change has to look like.ref Array[T], and a bare Array[T] refused) and D4 (& leaves) follow.A path’s head is one of five things, and a name that is none of them is
refused — ADR-183, closing
#141 and with it the other half of
NK1171.
nowhere::wobble lowered, and the language below answered failed to resolve: use of unresolved module or unlinked crate nowhere. Crate is not a word this language has, and the file it is about is one nobody wrote (Part III C.1).Summary::merge, Report::merge, Refused::NoStatement, Refused::NoDatabase, Op::Times, Op::Divide, Json::Null — and not one has a head this compiler has not read. So the case the entry named is one no program here writes, and the refusal has nothing to break.std module. Every one of the five is something this compiler had already read; the walk that asks them in order is what was missing.head:: answers for the head, because a description is a list of its items and no table carries the crate’s own word on a line of its own. Without that prefix, nikaia describe’s own output would be refused by the compiler that asked for it..nika file beside this one for a module, a [dependencies] line for a package, nikaia describe <head> for a foreign crate. That is the whole difference from unresolved module or unlinked crate, where the reader is told what failed and not what to do.rustc message, a wrong one costs a correct program.Shade::Even in main.nika names an enum that may stand in shades.nika, because the files of a package share one namespace (Part I 9.1) — and the tables the refusal asks first are this unit’s. Written against those alone it refuses a correct program, which is the one thing this checker may never do, and the same shape 0.0.130’s package had one construct over. tests/project.rs builds and runs that program, because a single-file test cannot see it: every unit test of the refusal passed while it did not build.tests/specification/COMPILES.txt recorded each of them as refused — by rustc, after lowering — so neither was ever a program. What changed is who says so and in what words: a head the block does not declare is this compiler’s sentence with a way out now, before the language below is handed anything. The two baselines together are what show it, which is why the page keeps both.NK1171 is a member a type this compiler read does not have; NK1181 is a head nothing declares. io::wobble is neither: the head is a module std’s ledger declares, and what that module holds is the ledger’s question (ADR-140 D5’s a module nothing describes yet, seen from the other side).A for lends, and everything downstream of the binding has to know it —
ADR-182, closing
#138 and #147. Two defects with
reproductions, and chasing them found three more — one of them a wrong
answer nothing said out loud.
for n in NS { n as i64 } answered casting &i32 as i64 is invalid, with dereference the expression as the way out: a noun and an instruction about a file nobody wrote (Part III C.1), and a way out that cannot be taken is not one (C.2).println(f"{text[1..3]}") answered the size for values of type str cannot be known at compilation time. The & form was fine, which is what kept it small and is what every program in the corpus writes.for, ADR-161 D6 takes a read apart with a *, and over a range what it takes apart is already a view. Neither decision is wrong here; what was missing is the sentence that says so at the cast and at the bracket.*, for the reason index::at is onenikaia_std::num::value(n) as i64, the identity for a number that is already a number and the number behind any run of views. A * is right at the binding and wrong one line down, where let n = 5 shadows it — and a * written onto that is rustc about the generated file, which is the very thing this closes, moved rather than removed.Local::lent) for the reason immutable does, and the predicate is the emitter’s own, shared, so the two cannot answer differently.* at a range and the * kept at a number: xs[0] is the element the program asked for, xs[1..3] is a run of them. Off the shape of what is in the brackets — a range is a run and a key is not — so the emitter still knows no types.& over one is the view it already is, or &dna[i..<i + k] becomes a &&str: the source’s & and the read’s own view are one claim written twice.*: at cannot pin a bare 1..=2, because At is implemented for a range of every signed type and all of them answer the same usize, so there was nothing to infer from. Handed over as written it settles itself — RangeInclusive<usize> is the only candidate that is a SliceIndex<[V]> — which is what the literal exception says everywhere else it applies. A range built out of names still converts, which is ADR-048 D1’s trade and k-nucleotide.nika’s shape.xs[-2..-1] is an access out of bounds and says so at run time (ADR-048 D1) — but -2 against the usize a slice wants is the trait Neg is not implemented for usize, about a type the program never named, and consider specifying an integer type that can be negative is a way out that belongs to a file nobody wrote. A negation goes back through the conversion, widened, so what the program gets is index out of bounds: the index is -2 at the line that wrote it.for over an Array[T, N] bound a name of unknown type, because element_of knew Vec[T] by its one argument and an array has two. Everything downstream went quiet with it — including the abort: for n in NS { n as u8 } over an Array[i64, 2] truncated silently, which is the one thing ADR-043 D4 exists to stop. A &[T] was the same absence (ADR-179 D1).let q: i64 = n over a binding was a &i64 where an i64 was declared. The number is read through the view; let copy: Row = r is not, because a number is Copy and a struct is not, so the same answer there would be a copy the source did not write (ADR-008 D5: a copy is written and never inserted). The way out exists and the program can take it, which makes that one a refusal this compiler owes rather than a lowering it is missing — #153.&[T] is the spelling to keep, and whether the view should be ref — both on open-decisions.md with the measurement that answers them: of the 98 &s in the corpus’s 2 847 lines, 82 are a type spelling and 53 of those are &str, and only 16 are a borrow the source writes, because ADR-094 D1 already took the word out of the place a reader meets it most.--comptime prints what was unrolled, and the unrolling reaches across the
files of a package — ADR-181 D5, which is
ADR-088 D6 and the last of that record’s
six decisions this compiler can supply. Part II 10.3 is built, less
T::variants.
--overlaps, --sharing, --tethers and --trust are it.describe in shapes.nika, called from main.nika, is the shape anybody would write Part II 10.3’s example in — and the build refused it, with the language below answering cannot find function describe in this scope about a function declared in the same package. A correct program refused (Part III C.4) by a claim that is false (Part I 9.1: the files of a package share one namespace, so there is nothing to import and nothing the author could have written differently).crates/nikaia/tests/project.rs: every unit test of the unrolling passed throughout, and so did the single-file path the corpus sweep exercises. The checker-level test beside it asks from both sides, because neither file is the one that knows.T::variants (ADR-088 D4), which wants a value nobody has designed, and NK1171 says which half is which.T::fields is a list the build walks, and the loop over it is unrolled per
type — ADR-181, building
ADR-088 D2, D4 and D5. Part II 10.3’s own
describe compiles and runs, which is the first time anything in that section
did.
NK1164 at the call. What the bound reaches was NK1171 — a refusal whose entire content was this is specified and this compiler does not have it, which was the only honest thing to say, since the member had no value, the loop had no unrolling and the body had no check.T::fields is known while the program is built, so a loop over it cannot be anything but unrolled (ADR-088 D4) — there is no run-time reading to rule out. The loop means what a loop always means; only the stage is earlier.describe__User, and no generic original: the generic body holds a loop over a shape, which has no form below. field.name becomes a literal and field.of(value) becomes value.name, so at run time nothing is left — no loop, no descriptor, no dispatch (D5’s what remains is the code somebody would have written by hand).fn tell[T: Struct](v: T) that never writes T::fields stays an ordinary generic, generic below and emitted once.describe(u) means is a question about one. One function writes the name at the call and at the definition, so the two cannot drift.field.of(value) is a String for one field and an i64 for the next, so there is no single type to check the body against — and a body wrong for one field is right for the others, on the same line. total + field.of(value) over a User is one finding, and it carries unrolling T::fields for User, at field name``. Without that line this is the error class C++ templates carried for twenty years.NK1180 is a member a reflected field does not have: .name and .of(value) are the whole of what Part II 10.3 gives one, and the list is short enough to print.T::variants is not built, and NK1171 now says which half is which rather than listing D4 to D6 as open: an enum’s shape is a different value, because a variant carries a payload where a field carries a type. The way out names match.--comptime — the report that prints what was unrolled, which is the same information the diagnostic carries offered on demand instead of on failure.A fixed table holds a declared type, as a view of one —
ADR-180, answering the second of the two
questions 0.0.125 put on open-decisions.md. That
page is empty of build-time questions now.
comptime PAIRS: Fixed[&str, Row] = [("x", Row { a: 1 }), …] was NK1127 — this compiler cannot evaluate it — for a value that evaluated perfectly well. Every part of it crosses on its own: a Row is a const (ADR-079 D1) and a table of &str keys is one (ADR-176). Only the combination did not.get’s shape, and it is 0.0.118’s own correction. Fixed<V>::get hands back Option<V> where V: Copy, because Option<&V> put a &&str in the generated file for a table of text and ?? over one is ambiguous between two Or impls. A Nikaia struct derives Clone and Debug and not Copy.Copy. So the table holds &'static Row and nothing about Fixed::get changes: the ledger still promises -> $V?, and what the program reads is a view of the row — the only thing a table living in the binary could ever have handed it. An enum is the same case.Copy is not derived, and that is the option not takenString field would silently remove a type from every Fixed that held it — and from the moves elsewhere that had quietly become copies.Copy struct is not moved at all, so the refusal ADR-118 D3 leans on would go silent for a whole class of types. ADR-008 D5 and ADR-107 D3 both say a copy is written and never inserted, and a derive is an insertion the source cannot see.Vec lowered. Fixed[&str, Bad] whose Bad declares Vec[i64] reached rustc as expected Vec<i64>, found [{integer}; 2] about the generated file — Part III C.1’s class, and the same hole the enum had at 0.0.125 one shape over. The walk that asks what a const cannot hold now reaches a pair, which is what a table’s rows are, so it is NK1167 naming the field.&[T] inside one gets its & (ADR-179 D2) and an Array[T, N] does not. That is the one place this package and the last meet, and the test runs it.&[T] is a type of this language — ADR-179,
the owner’s Option A on open-decisions.md’s question
of whether a build-time value may cross as a view into what the build allocated.
ADR-079 D1 has said since it was written
that a Vec[T] arrives as a &[T]; four records later it does.
Array[T, N] carries its length in its type. So a struct field can be one only where every value of that struct has a run of the same length — and struct Section { settings: Vec[Setting] } is the shape both corpus grammars produce. ADR-177 §5’s measurement, neither grammar in the corpus produces a result that crosses, is this sentence from the other side.asset_bytes could have, and Bytes owns its run.&str is exactly such a run and is what Part I 2.2 gives text.&[T] is still an address beside a count (ADR-147 D1, D2); everywhere else it is Rust’s own &[T], a fat pointer that carries its length. &mut [T] and &mut T stay the boundary’s alone, and NK1158 is narrowed to that half — the message now names the mut rather than the run..len(), xs[i], xs[i].field and for. std gains one impl, Get for [V], because [V] is neither a Vec<V> nor a [V; N] to the language below and a blanket one would overlap the map’s.Vec is lent to a &[T] parameter and the caller writes nothing — ADR-094 D1, the rule &str already had. What makes it happen is that a &[T] now answers is_a_view.[1, 2, 3] and &[1, 2, 3], and nothing in the value says which — so the type is walked beside it, down through a struct’s fields and a variant’s payload, and the & lands exactly where a slice was declared. comptime ROWS: &[Row] = [Row { … }, Row { … }] reaches the generated file as const ROWS: &[Row] = &[Row { … }, …];, with the array literal promoted to 'static by the const itself.pub rule file -> Vec[Setting] into comptime SETTINGS: &[Setting], which is the crossing ADR-177’s measurement said neither corpus grammar had. What makes it work is D3 — a parser builds a Vec, so the sub-program owns what the program views, and the dump is generated from the program’s declaration and reads a run either way.examples/config/src/main.nika still does not cross, and that is not a gap. It parses a file the user names on the command line, so what it holds has to be built while the program runs. The crossing is for a file the build names.NK1179, because the two lines look identicalVec[T] a body just built is not one, and there is no & the compiler may write for it: a struct outlives the expression that fills it, so the view would point at something already gone (ADR-107 D3). Without this rustc answered expected &[Setting], found Vec<Setting> about the generated file, which is Part III C.1’s class.& is the compiler’s, which is why this needs a message of its own rather than a fit that quietly holds.? the reader would have to write (Part III C.2). What it names is Vec[T], which is what a parse builds.comptime’s own literal is not this, which is the one line the type was added for: there the run is the build’s, and refusing it would be C.4.asset_bytes. The result type exists now and the read does not; ADR-072’s three-part naming is what it would be written under, and that is a package rather than a line.Tier-1 staging is withdrawn — ADR-178,
answering open-decisions.md’s question of whether the
box was still on the list. Nothing was built; a promise nobody intended to keep
stopped being counted as one.
staging-candidates.md names is closed, and three of them closed against that file’s own prediction. The table-driven html::escape it recommended was 16 % worse than the one-word mask that replaced it, because 4 KB of fat pointers is not a register. A template’s String::with_capacity is 3.4 % on a static page and 0.0 % on a growing table. And first-byte dispatch for literal alternations — which the survey called the actual prize — is under 3 % of the parse.loop: a reader who wonders where it went finds the record rather than nothing. The language area is counted out of 25 rather than 26 — 24.5 of 25, and 28.5 of 39 overall — and scripts/check-roadmap-numbers.py is what stops a withdrawal that forgets the number.[ ] on a progress page is a promise, and every honest one borrows credibility from the rest. That is the cost that decided it, not the cost of writing the record.crates/nikaia/tests/measure.rs, callgrind, ignored by default, with two workloads in benches/). It was built for this item and is the reason the item could be closed rather than argued about; what it measures is this compiler and the programs it emits.What a build-time value is, widened — ADR-079
§5 — after the owner asked whether a Vec and an enum come across from a
comptime, and then whether byte slices and compositions may. The answer
was partly, and measuring turned the two questions into five gaps, four
defects one construct over, and two absences that need a ruling.
comptime R: f64 = 1.5 had nothing to arrive as, so a comptime with a float anywhere in it was NK1127 — this compiler cannot evaluate it — for a line Rust writes as const R: f64 = 1.5;. The literal is written with {:?} and not {}, because Rust’s Debug for a float is the shortest text that reads back as the same bits, and only a finite one is written: 1.0 / 0.0 stays a refusal rather than becoming a const of something the source never named. Arithmetic over floats came with it, because a value that crosses and cannot be added is half a feature.enum variant, with what it carries. const A: Shape = Shape::Num(1.5);, Shape::Pair(7, "x"), and an array of them.[[T; M]; N]. The element type was read one level deep and asked no further.struct whose field is an Array[T, N]. The walk that asks whether a field can be written as a const read Array as a type with no constant form, which is the opposite of true.u8, the byte Part I 2.2 offers — so comptime MAGIC: Array[u8, 4] = [0x7F, 0x45, 0x4C, 0x46] is a buffer a const holds. It was NK1127.enum is what makes the refusal a question about the valueShape::Empty is a const and Shape::Many([1, 2]) is not, and the two are the same Shape. So a walk over the declared type has no answer to give. Without this the second one lowered — const M: Shape = Shape::Many([1, 2]); against a variant declaring a Vec — and rustc answered about the generated file, which is Part III C.1’s class. NK1167 now walks the value, names the variant, and says another variant of the same enum may cross perfectly well.(f 1.5) and a match with an arm per variant emitting (v Ty Variant …); a variant with named fields is refused by name, because a build-time value carries a payload by position. The decoder had one defect of the kind this format invites: a variant’s tag is two words and the reader took the second from where the first ended, so (v Shade Odd) came back with an empty variant name. A test that runs a program is what caught it..nika file and unable to build the shape anybody would write it in. Project::report rebuilds the source map whenever the backend had anything to say — and anything includes a warning, which the generated crate emits for the grammar! macro’s cfg — and it rebuilt with Reads::none(). That pass evaluated the comptime to nothing, the emitter refused an item the build had already written a const for, and this compiler’s own internal sentence reached the user in place of the program running. One function now answers what may this build read, and where does it compile a parser for both lowerings, and tests/project.rs builds and runs a grammar project — because every unit test passed throughout.xs[i].field did not compile, for a Vec and an Array alike. (*index::get(&rows, index::at(1))).a is type annotations needed: at’s I has nothing to infer itself from, and where the element is only printed an integer literal’s late defaulting settles it, while a field read on the element needs the type before the defaulting happens. The emitter already knew — the paragraph beside the write branch says at(0) has nothing to infer I from and takes the literal out of the conversion — and the read branch had never had the same exception. A nested index is the same absence one level out, and grid[1][0] works now too. A range stays in the conversion, because Get<I> for str wants a Range<usize> that a bare Range<{integer}> does not give it.Shape::Spot { x: 1 } answered Shape::Spot, which is not a type any declaration can be written as — so a correct function was NK1104, hands back Shape::Spot and declares Shape, with a way out that cannot be taken (Part III C.2). Its fields went unchecked in the same breath, because nothing held them under that name. Both halves are one map now, built where the enum is read.let x: u8 = 300 lowered, which is the C.1 hole a crossed form opens if the range is not checked with it. NK1116 has the byte’s range now, and the article is read off how the name is said: a u8, an i32.Vec[T] inside a crossed value — a field of a struct (examples/config/src/main.nika’s Section { settings: Vec[Setting] }) or a variant’s payload (examples/json/src/main.nika’s Json::Array(Vec[Json])), which is what both corpus grammars produce — cannot cross: Array[T, N] cannot type a position whose length differs per value. A binary asset cannot be read at all (asset("…") hands back a &str and refuses a file that is not UTF-8 by name, and there is no result type an asset_bytes could have), and Bytes owns its run. All three want ADR-079 D1’s own &[T], which that record has carried as not-built since it was written, and the question is on open-decisions.md.struct in an array crosses, an Array[T, N] as a struct field crosses since this package, and an Array[u8, N] is the byte buffer &[u8] would have been. What a Fixed cannot hold is the composition, and that is the next bullet.struct as a Fixed value is its own question, with a different root: Fixed<V>::get hands back Option<V> where V: Copy — 0.0.118’s own correction — and a Nikaia struct does not derive Copy. That page recommends the message first, because NK1127’s cannot evaluate is the wrong claim: the value evaluated fine.for binding is a cast over a view (for n in NS { n as i64 } is casting &i32 as i64), and a slice of text read as a value does not lower (f"{text[1..3]}", where &text[1..3] is fine and is what the corpus writes).A grammar runs while the program is built, by compiling the parser it generates — ADR-177, and Part II 10.2 A is a program.
winnow-grammar is a code generator: its model crate parses the grammar language, validates it and hands the result to a macro that writes a parser. There is no interpreter in it to borrow, so interpreting here would be a second implementation of the same semantics, and Part II 10.2’s one grammar, two stages would stop being a property and become a hope.comptime SETTINGS: Array[Setting, 2] = Cfg::file(asset("app.conf")) reaches the generated file as const SETTINGS: [Setting; 2] = [Setting { key: "host", value: "example.com" }, …]; — with the grammar’s own s.trim() already applied, and no parser left in the program.asset("…") and ADR-072 says which. Neither the permission nor the input is new here.NK1178, one code and six sentences, because what a reader can do differs completely. Invalid input fails the build in the parser’s own words — relayed whole rather than re-worded, because it is in the grammar’s vocabulary and counts its line and column against the bytes that were parsed. A result with no build-time form is refused from the rule’s declaration, before a parser is compiled: there is no point building one to learn that its answer has nowhere to go. A grammar inside a grammar would have this compiler start a second compiler inside the first.const cannot hold one — Rust holds const X: f64 happily; it is refused because a value this compiler carries while it builds is a whole number, a bool, text, a list or a struct, and there is no float among the five. An enum variant is the same absence one shape over.examples/json/src/main.nika’s Json is an enum with Vec payloads; examples/config/src/main.nika’s Section holds a Vec[Setting], a field that owns memory. So the mechanism runs and ADR-079 D1 is what stops both.Json::value(asset("config.json")) used to be NK1127 — this compiler cannot evaluate it — and is NK1175 without a list and NK1178 with one. Which is this working, and the crossing being the next question.structs whose fields are text — the shape a configuration file has, and the shape the database driver (#89) wants.struct this program declares is its own name below, in an array as well as alone: const ROWS: [Setting; 2] needed rust_constant_type to know a type it cannot know, so the checker answers it — that function has no program to ask and this has one.A file a build reads is named three times —
ADR-072 entire, with
ADR-116 D2’s asset("…") under it. The
record said built: no because const has no syntax, so there is nothing to
check yet; the order it implied has been walked, and the check finally had a
const to hang on.
comptime CONFIG: &str = asset("config.txt") reaches the generated file as const CONFIG: &str = "…"; — the file’s text, read while the program is built. asset is the compiler’s and not std’s, so it is recognised in a comptime initialiser and nowhere else.--allow-read-from-list=FILE, one path per line, # begins a comment. It reaches the rustc wrapper through NIKAIA_ALLOW_READ_FROM_LIST, because the wrapper is a second process that lowers the same file again and two halves of one build must not disagree about which files it may read. That is also how D6 holds: every member of the workspace is lowered under one environment, so a dependency’s read is checked against the list of the build that is running — a package cannot bring its own permission.const holds. Bytes that are not UTF-8 are refused by name rather than converted.NK1175 — one claim, five reasons, and the way out differs with the reason because the three namings are not derivable from one another: add the flag is not the answer to a path missing from the list, and add the line is not the answer to a build run with the reads switched off. It also covers a path that leaves the project root, a file that is not there, and bytes that are not text.NK1176 — a path the build worked out (D4). Answered before the argument is evaluated, and the test that says so uses a path which is listed, is on disk and would have been allowed: if the check ran on the value there would be no finding at all, and named in the code would have quietly stopped being decidable by looking at the line.NK1177 — an asset written outside a comptime. Three words each decide one thing: comptime says when, asset("…") says where the bytes come from, and the call around it says what is done with them. The way out names fs::read, because a file read while the program runs is what a reader who wrote it there almost certainly meant.from is an ordinary name — ADR-116
D1 and D3, and the drift that fell out of checking it.
from is out of parser::RESERVED_WORDS and out of the grammar’s RESERVED alternation, so it is a name in every position that declares one — a field, a parameter, a let, a member. Part III 17.1’s pub fn rename(from: Path, to: Path, …) parses as the page writes it, which is D3 and the reason the word was worth taking off the list: it is the most common name on it.dsl X from e still gets a grammar is entered by a call rather than a parse error at whatever token is next. That sentence comes from the grammar’s token and never came from the reserved list — the two are different things, and telling them apart is the whole of this change. ADR-116 D1’s loses nothing is literal.select arrived with its construct (ADR-148) and reached parser::RESERVED_WORDS and not the page, so a reader counting the words there got a different answer from the compiler — for two months.select is owed, the one every word reserved for a construct has.with — a copy of a value with named fields changed —
ADR-118, Part I 4.2. A word that had been
reserved since 0.0.8 and had no rule.
p with { x: p.x + 1 } is a new value of the same type, with the named fields changed and the rest taken from p. One postfix rule, so D1’s any expression of a struct type is exactly what stands to the left of one, and D2’s p with { pos: p.pos with { x: 1 } } is that rule twice.user with { name }), and the refusals. A field the type does not have is NK1107 and a private one NK1110, both reused rather than written again.Point { x: 4, ..p }, and never ..p.clone(): no copy is inserted that the program did not write (ADR-107 D3).NK1173 is the operand, which is the whole of what is new. One claim, four reasons, each with a way out that can be taken: an enum, whose fields depend on a variant the type does not say — and the way out names match, where a variant is known, which is the decision ADR-118 §4 deliberately left open; a view, because the unnamed fields are taken by move and there is nothing to move out of one; a type with no fields; and a value whose type this compiler did not work out.comptime’s value already makes (ADR-011 D2). A with with no entry never reaches the emitter, because NK1173 refused it first.NK1174 is a with that names no field. D1’s reason is about meaning — a copy that changes nothing is a line the reader would puzzle over — so it is read by the checker. The parser could refuse { } and did for an afternoon; its caret landed on the line after the braces, because by then it had consumed them and the whitespace behind them.Point { x: 1, x: 2 } used to lower, and rustc answered about the generated file — Part III C.1’s class. The rule is the literal’s and the record only restates it for with; NK1172 is written once and both come to it, because one rule written twice is two rules waiting to disagree.examples/1brc.nika’s Stats. Reading it says no: Stats changes in place through &mut self — add and merge are written that way because they run inside par_iter — and a with there would mean returning a new Stats from each, which reads worse and moves a benchmark’s call sites. Nothing else in the corpus copies a struct with one field changed.The two bounds that ask what a type is — ADR-088 D2 and D3, the first part of Part II 10.3 to be built.
[T: Struct] and [T: Enum] are bounds this compiler answers, and what answers them is the declaration rather than an impl. That is D2’s whole point and the reason they could not be ordinary traits: no impl Struct for Point exists to be written. NK1135 used to refuse Struct as a trait nothing declares, which was true and was not the answer.NK1164. That is the one class of failure a bound exists to move out of the body — the record cites Zig’s anytype as the counter-example, where the signature says nothing and the caller learns its type does not fit from a message pointing into somebody else’s code.impl, because impl Struct for Op is not a line anybody can write and Part III C.2 says a way out that cannot be taken is not one.std type, a foreign one, a name no ledger classifies all look alike from inside the checker, and Part III C.4 says a correct program refused is the worse mistake. What stays refused is what it has read: the other shape, and the types Part I 2.2 offers.Struct in this scope, about a file nobody wrote. The two names live in one constant, types::SHAPE_BOUNDS, because the checker, the bound check and the emitter each have to know the same two.trait Struct { … } keeps its own. Every reader of the two names asks the declaration first, so the bound travels then, with the impl that answers it — which the emitter has to get right, since dropping it there would be rustc’s no method named label.T::fields went from NK1135 on the line above to silence the moment the bound became legal, and from there it would have gone to rustc. NK1171 says it instead, and says which half is built: the bound is, and D4’s unrolled loop, D5’s per-iteration check and D6’s report are not.comptime initialiser evaluates calls, methods, for and while, and hands arrays, text, structs and a fixed map to the program below. So what 10.3 waits on is a type’s shape as a value, not the machinery to walk it, and #174 now lists the four steps in order.fn describe[T: Struct](value: T) used to be refused with NK1135, on the bound; it is refused with NK1171 now, on the member — which is the baseline recording one line of progress.A name beside a type this program declares — a defect ([#141](https://github.com/Nikaia-Language/Nikaia/issues/141) §1), found while reading what Part II 10.3 would need.
Op::Mul, written against enum Op { Add, Sub }, used to lower. The compiler had read the declaration, the exhaustiveness check was holding the variant list in its hand, and nothing compared the two — so what refused the program was rustc, about the generated file, which is Part III C.1’s class and the one this compiler refuses on principle. NK1171 says it at the line that wrote it, lists what the type has, and names the one that was probably meant when a name is close.else arm beside it, it reports nothing at all.Event::Chose(Op::Mul) is the same mistake one level down. A check that stopped at the outer path would be worse than none — a reader would learn that it exists and then meet rustc anyway.::. Point::x is NK1171 too, with the way out that works: a field is read from a value.Point::fields says that Part II 10.3 is specified and unbuilt. A reader who writes it has read the specification, so Point has nothing called fields would send them looking for a spelling that does not exist. Part III C.2 asks for a way out that can be taken, and the only one here is writing the fields out — so that is what it offers, rather than a rewrite of the same line. ADR-088 §5 is the record it cites.nowhere::wobble still lowers. A module of a package, an item of a foreign crate and a name no ledger has been told about all look the same from inside the checker, and refusing on absence would refuse correct programs.Summary::merge, Report::merge, Refused::NoStatement, Refused::NoDatabase, Op::Times, Op::Divide, Json::Null — and every one is a variant of a declared enum or a key a ledger records. Not one has a head this compiler has not read. So the unfixed half is a case no program here writes, and it can wait for the mechanism that would make its refusal right. #141.A map the build can see — ADR-176,
answering open-decisions.md’s what does a program
write, for a map the build can see? with option B, and closing the last
unbuilt part of ADR-079 §3.
comptime ROUTES: Fixed[&str, i64] = [("get", 1), ("post", 2)], and no new syntax. The list literal is built, the tuple is a type this language has, and the declared type is what makes a list of pairs a table — which is ADR-152 D4’s a use answers a literal, the same sentence one shape out. A map literal was the other option and stays possible: it would find this crossing already built under it.Fixed[&str, V] in std, with get, has, len and is_empty. ROUTES.get(k) is a V?, so ?? 0 is how a miss becomes a number (ADR-161). The comptime lands as one const holding four &'static tables.Value::Tuple in the evaluator, which is also what ADR-088 D1’s field walk wants, so the two share the step.comptime list of text crosses as [&str; N]. That is ADR-079 D1 rather than an addition to it — a list crosses as an Array[T, N] and text as a &str, and an array of text is both at once. What asked for it was walking a table’s keys with a for.match under a threshold and a hash table over it, because fixed-map-lookup.md says a match beats a perfect hash for small key sets. §6 then measured the third shape nobody had: a linear scan over the very same static arrays. It is the match within 0–17 % up to twenty-four keys — a dozen past where the hash has already taken the lead — so the range where generating a match would pay is empty.Fixed is a value a program may pass and store, the emitter stays one that knows no types (ADR-011 D2), and which shape a table has lives in the data: no displacements is the small one. Neither the compiler nor std holds a branch that says this table is small.NK1169 — a key written twice. [("get", 1), ("get", 2)] has no meaning a compiler may pick between; the error names the key and both pairs. This is what a comptime is for (ADR-073 D3): a duplicate in a routing table stops the build instead of resolving to whichever the map kept.NK1170 — a key that is not text. A number or a bool wants a different table, most likely a dense array, which is a different decision with a measurement of its own. Both ways out are real: text keys, or a collections::HashMap built while the program runs.NK1127 behind them — this compiler cannot evaluate ROUTES — which is not a second fact and whose way out (write let ROUTES = …) is wrong advice for a mistake the compiler just read well enough to name. Same handover ADR-175 D2 made for NK1165.bool was four packages out of date; at item level it is a parse error was false, and it was the reason the section gave for its own example not working; and not implemented are push and aggregate values had been built at 0.0.108 and 0.0.111. All three now say what the compiler does, and the section carries the Fixed form.Fixed::len joins the five names that are lengths. contracts.rs lists them by hand, so adding one is a decision rather than a line — the emitter writes as i64 because the name is len, and an entry promising an i64 under any other name would emit a usize and nothing would notice.Fixed::get hands back a value, not a view of one, which is what std.contracts promises (-> $V?). Option<&V> put an Option<&&str> in the generated file for a table of text, and ?? over one is ambiguous between two Or impls — rustc complaining about a file nobody wrote, which Part III C.1 does not allow. A table of i64 never showed it, because one of the two impls happens to fit there.comptime EMPTY: Fixed[&str, i64] = [] was NK1166 — this is a Vec[?] and the const says Fixed[&str, i64], a correct program refused with a way out that asks the reader to write what they already wrote. ? fits everything (ADR-024 D1), which is how the array crossing one shape over had read it all along: comptime XS: Array[i64, 0] = [] lowered the whole time.crates/nikaia/tests/fixed_map.rs runs the program, over both table shapes and over every key of the hashed one. The compiler’s FNV-1a and std’s are two implementations of one function in two crates with no type between them, so nothing but a running program holds them together — a generator that agreed with itself would prove nothing. Checked against a deliberately broken hash: the running tests go red, the shape tests do not.The fixed map’s threshold is measured — ADR-079
D3 said how it is looked up is the compiler’s and named no number, and
staging-candidates.md ends on the rule that says
one is needed before any of it is built.
docs/history/fixed-map-lookup.md: the crossover is between 8 and 16 keys. At eight a match is ahead by 2.4× on a hit and 4.3× on a miss — a handful of keys is ruled out by length and one comparison, and no hash beats that. From sixteen the perfect hash is ahead on both paths and never gives the lead back: flat at 12–14 ns whatever N is, where the match climbs to 25 at 256.match against a HashMap — 9.05 against 18.65 on 200 keys — and ADR-079 §3 reads it as 200 keys favour a match. Against a perfect hash, which is what a compiler that owns its key set can build, a match of 200 keys is behind by 1.9×. That record now says so.match rules a miss out by length where a perfect hash pays one hash either way.staging-candidates.md §5 warns about picking a benchmark that does not exercise the change; this is the same mistake one level down, and only a fairness check found it.open-decisions.md. The lowering is now decidable and the input is not: a program builds a map with collections::HashMap() and insert, both std’s, whose bodies are Rust — and there is no map literal. Three options, and the recommendation is the one that adds no new syntax: a list of pairs whose declared type says it crosses as a map, which is ADR-152 D4’s rule already used twice.A constant is an item, and an item is visible wherever its file is — which answers 0.0.115’s one deliberate limit and a defect that had been sitting beside it.
comptime A: i64 = B * 2 over comptime B: i64 = 21 was NK1117, nothing declares B — on a line whose successor declares it. That is a correct program refused (C.4) with a sentence that is not true, and it was odd twice over: a function declared below its caller has always been callable, from a comptime included, because items are order-independent. A constant was not, because the walk that binds them goes down the file.NK1168: a constant worked out from itself. The cost of the above: once a constant may read one declared later, comptime A = B beside comptime B = A becomes writable, and it has no base case to reach. Not the call depth (ADR-075 D4’s neighbour), which catches a recursion that would end if the stack were deeper and says so — this one never would. The message names the ring: the ring is B → A → B.item_constants declares every name before it evaluates any, which is what makes an item behave like one. The type is the declared one where there is one and ? where there is not — which says nothing, and is what a name this walk has not reached the value of honestly is.A comptime reaches across a file boundary — and the ledger needed nothing
new, which is the part worth saying first.
doubled two files over passed the contract check and died afterwards.Around beside the two facts it already carried.Parsed that owns it, which is the hazard that made this more than plumbing: every parse_to_ast builds its own interner, so a symbol from another file resolves to nothing — or to the wrong text — when read with this one’s. The file is swapped for the length of the call and put back after, which is what makes a chain across three files right as well.comptime TOTAL: i64 = Point { x: 3, y: 4 }.doubled().sum() where Point and both methods are declared in another file; banner("nikaia") returning an interpolation built there; and through(5) calling from file to file to file. The test runs the program and reads 14 [nikaia] 51, because a struct literal, an interpolation and a three-file chain are exactly the shapes an interner mix-up would show.comptime came to is the checker’s answer and the emitter writes it, so both now get the program’s files — a unit lowered against fewer files than it was checked against would refuse an item the check accepted.comptime beside the body that reads it.A sync method of this program’s own folds now — the wall the owner asked
about two packages ago, and the reason for it was never sync.
Value::Struct, and with it the method. sync is the permission (ADR-075 D1) and its ledger check applies to a method’s key exactly as to a function’s — Reader::read that reaches the world is NK1152 by that key. What stopped a method was that it needs a value to be called on, and this evaluator had none to make.Point { x: 1, y: 2 }.scaled(10) folds; a method may call another on self; and a method is found by the key a call resolves to, which is the ledger’s own split, so the two cannot drift about which name a call means.const ORIGIN: Point = Point { x: 1, y: 2 }; is Rust, and a struct whose fields own nothing is already its own view — ADR-079 D1’s a number is already its own view, read one shape out.comptime binds its whole value, not the integer it would have been. constant is the fold’s and stays; built is the value, which is what comptime BIG = ORIGIN.scaled(10) needs one line later.NK1167 one level in: `B`'s `items` is declared `Vec[i64]`, and a `const` cannot hold one. A reader cannot see which half of Bag { items: [1, 2, 3] } the language below refuses, and this cannot be evaluated leaves them to work it out.items: Array[i64, 3] was told to declare Array[T, N] — the same failure this session criticised two packages earlier, met from the inside. It asks the declaration now, and the test takes the way out and runs the program.NK1127’s catalogue says a method of a struct declared here as well as a function of this file. What still does not fold is std, whose body is Rust, and a callee in another file of the same program — the one-file limit of this walk, which #174 carries with the interner hazard that makes it more than plumbing.A build-time text is the value, not the spelling — 0.0.112’s refusal of
.len() over text was a representation showing through, and the owner asked why.
"\u{0041}".len() is 1 and "\u{0041}" == "A" is true. 0.0.112 held text as the source wrote it — escapes and all — and refused both, on the grounds that they ask about the value where it held a spelling. That is a true sentence about the representation and not a rule of the language, which is what made it the wrong answer.STR_CHAR takes \ and any character and keeps both, and the emitter writes a .nika literal into the generated Rust verbatim — so rustc decides, and println("a\qb") is unknown character escape on the .nika line. The escape set is the backend’s.comptime BUILT: &str = "a\tb\nc \"q\" \u{0041} \u{20AC}" against the same literal read at run time, printing true true for the text and for the byte length.NK1127’s catalogue is right for a shape this evaluator does not read and is the wrong answer everywhere else — it invites somebody to go looking for the spelling that works when there is none. A method is not a shape it reads at all (which is why "a".to_uppercase() fails, and a sync method of the program’s own fails the same way); a callee with no entry is std’s or a package’s, whose body is not this language’s to run; a callee the ledger describes but this file does not declare is the files-share-one-namespace limit of a walk that reads one file.sync is the permission and not the ability, said out loud in the note, because that is the confusion the question started from: ADR-075 D1 says a body may run while the program is built, never that this compiler can run it.std method, which would be just as unreadable one function further in. Each wall carries its own, and the universal let escape names the binding as the generic refusal always did.comptime GREET = "hallo" at 0.0.112, then "x".len() and "abc".len() here. That is the guards working rather than failing: a test that records an absence is the thing that notices when the absence ends, and each moved to a method this evaluator still does not read.Text while the program is built — the thing NK1127’s own note had been
calling out as missing since the evaluator gained a call, and the other half of
ADR-079 D1.
comptime may hold text, and it crosses as the view: comptime BANNER: &str = f"nikaia {MAJOR}.{MINOR}" reaches the generated file as const BANNER: &str = "nikaia 0.1";. A String allocates and const X: String is not a thing the language below has, where const X: &str is — and &str is the spelling Part I 2.2 already gives text, so unlike the list half nothing had to be invented for it.f"…" is read, and it is what makes this worth having. A hole is Nikaia (ADR-032 D3), so this evaluator reads it the way every other analysis does. A banner built out of two other constants is the case a person writes; a literal alone is a value they could have written down. A format spec — f"{n:>8}" — is std::fmt’s meaning rather than this language’s, so it stays a shape the evaluator does not read.+ over two texts, which is Part I 4.7’s operator and the only one this holds an answer for."\u{0041}" and "A" are one value and two written forms, so == and .len() over text are refused rather than answered wrongly. A decoder is what they want, and nothing has asked for one.NK1167 gained its text half (ADR-079 D2): a comptime declared String is sent to &str. What it used to get was the ordinary type rule’s write .to_string() — advice that makes the problem worse, since a String is the one thing a const cannot hold.comptime GREET = "hallo" was the example in what does not fold is refused, because text was the whole of what a string literal got; the example moved to .to_uppercase(), which still cannot fold and is in the same family on purpose, and the literal has a test of its own saying it folds.A build-time table may be grown with push — which is
ADR-079’s own title, growable going in,
fixed coming out, and which 0.0.108 said was not possible.
xs.push(v) in a build-time body. fn squares() -> Vec[i64] { let mut xs = []; for i in 0..<5 { xs.push(i * i) }; return xs } under a comptime TABLE: Array[i64, 5] reaches the generated file as const TABLE: [i64; 5] = [0, 1, 4, 9, 16];. The body works with a list that does not know its length; what crosses into the program is fixed, and the declaration is where it becomes so.NK1167 (ADR-079 D2): a comptime whose value owns memory, refused for what it is — and the way out names the number, which is why it is worth a code rather than a note. The build has just computed the value, so declare it Array[i64, 5] is a line the reader can take instead of one they have to work out by reading the body.NK1157 gained a second sentence for the same rule. An Array[T, N] takes exactly N elements whether the N was written or computed; what differs is the way out, because write five elements is advice nobody can take about a number that came out of a body. It says this computed 5 elements, and the array holds 3.push was too narrow, and the record says so where it stands. The measurement behind it was right — .push hands back a Vec[?] and NK1104 refuses that — but what NK1104 refuses is a function’s declared result, and a comptime is not one. By the time a declaration is compared, the build has computed the value, so its length is a fact. ADR-175 D1’s heading carries the correction and ADR-079 is built for a list now.comptime TABLE = squares() over a -> Vec[i64] body wrote const TABLE: [i32; 5], because the narrowest type that holds 0, 1, 4, 9, 16 is an i32 — and the first i64 arithmetic on it would be rustc’s complaint about a file nobody wrote. The checker’s element type is used where it has one; Part I 2.4’s widest-holder rule stays the fallback for the case nothing declared anything.NK1152 says the callee may not run while the program is built; this is a Vec[i64] and the const says Array[i64, 1] used to follow it, pointing at the one line that is right. That is C.4’s failure with the refusal already made. Every refusal at a comptime now suppresses NK1127 by the same counter rather than by a rule remembered per site.Two of the three points the language is short of are not work, and the page said they were. No code changed; what changed is that the roadmap stops sending a reader to a design space that has been decided and to a direction the project examined and did not take.
const. That question is answered: ADR-075 answers Q4 and with it Q1, Q2 and Q3, because a build-time body may call what is sync and touches at most the build’s own parameters — two ledger columns the compiler already derives, so the sandbox the record asked somebody to invent is not needed. ADR-072 answers Q5 and the filesystem half of Q6. ADR-026 stays Open for one thing: what bounds a build-time body’s memory.action blocks are arbitrary Nikaia, so evaluating one at build time means running user code at build time — #178 says a grammar must not be interpreted here at all: winnow-grammar is a code generator with no interpreter in it, so interpreting would be a second implementation of the same semantics and Part II 10.2’s one grammar means the same thing at both stages would become a hope. What to do instead is compile the generated parser during the build and run it, which needs no new security model.comptime PORT: i64 = Cfg::value(from "port.txt") is a parse error at the from. ADR-072’s built: no gives its reason as const has no syntax, and comptime has had syntax since ADR-097 — so that record’s own order is satisfied on both counts and the work is startable.open-decisions.md, with what the survey behind it measured: all three candidates anybody wrote down are closed — two built and both std optimisations rather than the thesis, the third profiled at under 3 % against the 56 % that produced a merged −19.8 % — and that file ends on a rule, a staging decision enters the compiler only together with a measured crossover. So the box is not unbuilt work; it is a direction examined and not taken, and whether that is a withdrawal is not this page’s to decide. The recommendation is A, withdraw it with a record, with C — re-write the box and keep the door — as the cheap half if the answer is not yet rather than no.Every refusal this compiler makes about a line of a .nika program names that
line — ADR-171’s remaining eighteen
sites, which that record’s §4 left open as a different walk.
extern ABI, a comptime with nothing to write, and the catch-all — only had to use it. Measured: extern "stdcall" { … } now says so on line 1 with a caret under it.pub (ADR-082 D2’s shape). Both callers hold a Flow, which is D1’s own handover.dsl family: a target that is no grammar this compiler has, and the two takes no context refusals. With them, the five refusals inside template.rs — which work on text and never saw a file, so one at_the_statement covers all of them at the one caller that knows where it is.Self::dsl in a result position gets the body’s first statement, which is the nearest place that walk has and is the same function the reader is looking at.--target or a user-parallelism nobody can spell is about a manifest key and a whole build, and a line number for one would be an invention — tests/refusal_lines.rs asserts the absence, which is the half a conversion like this can quietly get wrong.project.rs and manifest.rs are the same case one level out: each names the nikaia.toml or the directory it is about, which is the place it has.[x], and its nine of thirty-three was stale in the other direction too — ADR-171 converted fifteen. What stays open is expression-level spans, which is a refinement of a decision rather than a shortfall: reporting on the enclosing statement is ADR-024 D7’s own answer, and both walks get narrower the day expressions carry spans without either changing. The language reads 23.5 of 26, 27.5 of 40 overall.A build-time value may be an array (ADR-175) — ADR-079 §3’s evaluator that can loop and push, which three records have been waiting on. The loop was built; what a loop had nowhere to put was a value.
comptime TABLE: Array[i64, 5] = squares() is const TABLE: [i64; 5] = [0, 1, 4, 9, 16];. The evaluator gained the four things writing one needs and nothing else: the list literal, xs[i], xs[i] = … and xs.len().push, which is measured rather than chosen. .push on a list hands back a Vec[?]; a Vec allocates, so a const cannot hold one, and NK1104 refuses it against an Array[i64, 5] long before this evaluator is reached. [T; N] is exactly what a const holds (ADR-152) — so a build-time table is written at its length and filled by index, which the type system already made spellable.NK1165: a build-time index the array does not have. xs[7] of three elements, in a body being run while the program is built. ADR-048 D1 aborts with this sentence at run time; here there is no run to abort in, and this compiler cannot evaluate it would send the reader looking for a missing feature rather than at the line.Array::len and Array::is_empty in std.contracts. An Array[T, N] knows its length everywhere except in the ledger: xs.len() on one resolved to no entry, which lowered and ran and cost the enclosing function its touch set, because an unresolved call is unknown. The visible price was that for i in 0..<xs.len() — the natural spelling of a build-time loop — made a function uncallable from a comptime with nothing says what it touches, while 0..<5 was fine.comptime PRIMES: Array[i64, 4] = [2, 3, 5, 7] reached expect with the word "const", which had no diagnostic code, and hit an unreachable!. That is Part I 6.8’s a raw internal error reaching you is a Nikaia bug, met by the compiler on itself. The word has NK1166 now — and the literal it crashed on is answered by its annotation the way a let’s is (ADR-152 D4), which was one line present in the let path and missing here.NK1127 behind it: NK1152 said which callee the rule forbids and then this compiler cannot evaluate it said the same thing with less in it. NK1127 is kept for the case it is about — a shape the evaluator does not read.tests/contracts.rs’s length guard names five entries. That test exists so a new ::len is a decision rather than a line — the emitter converts xs.len() to xs.len() as i64 by name, and a signature promising an i64 while emitting a usize is what nothing else would notice. It caught the fifth, which is what it is for.Two roadmap boxes were describing a compiler from several records ago. No code changed here; what changed is that the page says what the tree does, and each claim was re-measured rather than remembered.
[x], and all four things it listed as open are built. D6’s Send as a refusal of ours — held_across_a_pause answers what a task binds and then holds at a suspension point, which the capture check never asked; it refuses nothing today and that is the answer, because no type is MayNot at Destination::Ours and a lock is MayNot only at a foreign one, where ADR-061 D1’s NK2503 does refuse by name. Standard input suspends (0.0.94). A recursive pausing method is boxed. And a pausing lambda is lowered where the parameter is declared in this language (ADR-122 D1, measured: |p| Box::pin(async move { … })).std takes a lambda in ten places, and in none of them would a pausing lambda be a correct program. Six lower to Rust’s own synchronous closures (map, filter, sort_by_key, and_modify, or_insert_with, ListExt::map); the other four run their lambda while a lock is held, where pausing is refused on its own merits. The first program to meet the limit is the one that binds a route handler, which is the HTTP server’s box and not the language’s.Macro Expansion (JIT) is renamed to The build-time evaluator and is [~]. Both halves of placeholder exists, needs implementation had stopped being true: ADR-117 took macro and quote off the reserved list, so there is no macro feature to implement; and build_time.rs evaluates a comptime that calls a function of this program with if, let, for, while, break, continue, assignment and return in its body. Measured: a ten-step loop reaches the generated file as const LIMIT: i64 = 55;.push and no aggregate value, so ADR-079 §3’s table can be computed and has nowhere to arrive. The types stopped being the obstacle two records ago — ADR-152’s Array[T, N] is exactly what a const can hold.A bound is enforced where it is used — ADR-174, the second half of what a bound is for, and the one nobody was asking.
Ledger::implementations: a trait’s name to the types whose impl names it, written where the impl stands and merged when a program’s ledger absorbs its units’. The ledger already said Speaks is a trait, which is what NK1126 needs; it did not say Dog answers for Speaks, which is what a call needs, and that claim is made in exactly one place. Not written to the ledger file, for traits’ reason: nothing outside the unit can name one of these traits, so nothing outside it can ask.NK1164: the type a call picked does not answer for the bound. fn tell[T: Speaks](x: T) called with a Rock that implements nothing was accepted by every stage of this compiler, and the language below answered the trait bound Rock: Speaks is not satisfied about a file nobody wrote — Part III C.1’s class. Asked at the free-call and the method-call site, from the binding both already build for the other question a type parameter raises (ADR-074 D2).impl. A parameter of the caller’s own cannot be — V is a name a further caller fills in, and impl Speaks for V is not a line anybody can write — so it is told to widen its own bound: add it to the bound: [V: … + Speaks]. C.2 owes every diagnostic a way out, and a way out that cannot be taken is not one.impl Error for ConfigError names one the compiler reads, and traits::check draws the same line), an argument this compiler could not type, and a parameter that carries the bound.impl Speaks for Dog stands beside the fn tell[T: Speaks] rather than in it was refused — and it builds and runs, measured, printing woof. That is what moved the table into the ledger, where a program’s own is absorbed from its units’. C.4’s correct program refused is the failure a refusal is most expensive in, and it was found by asking what the change does to the shape the corpus does not have.[x]: a type parameter is written rather than erased, a bound puts its methods in reach of the body and decides what a caller may hand in, and the refusals are Nikaia’s by name. 22 of 26 in the language, 26 of 40 overall.A field of a generic impl reaches the body as a type, which is the half of
ADR-074 D1 that was written down and not
held — and three refusals the language owes came back with it.
self inside impl Holder[T] was typed as a bare Holder, binding none of the declaration’s parameters. So self.value’s T was substituted against an empty map and came out ? — and ? is the one thing this checker says nothing about. The receiver is built from the head’s own brackets now: Holder[T] inside impl Holder[T], Holder[i64] inside impl Holder[i64].self.value.to_uppercase() on an unbounded T is NK1126, the same sentence a parameter gets. return self.value where the method declares -> i64 is NK1104. return self.value out of a &self is NK1131, whose way out — declare the result &T — is a type the program can write, and the test compiles the lowering to say so.rustc before, about the generated file, which is Part III C.1’s class exactly — the one the generics tests were written against in the first place.impl Holder[i64] now types its fields as i64, which is the other half of reading the head’s brackets: arithmetic on self.it was arithmetic on a ?.trait Speaks, impl Speaks for Dog and [T: Speaks] are built and check against each other (ADR-078) — but nothing asks whether the argument’s type implements the bound, so tell(rock) where Rock implements nothing is accepted here and refused below as the trait bound Rock: Speaks is not satisfied. C.1’s class again, and the reason the roadmap’s generics box is a half rather than done.[T: greet::Speaks] is a parse error at the :.rustc-tested since ADR-074 — while open was too generous about the bound. It is [~] now, with what makes it a half written in it: 21.5 of 26 in the language, 25.5 of 40 overall.impl table’s union are not.The README stops being a second copy, and the roadmap says how far along it is in numbers a reader can re-derive.
examples/ stands where it was. A server that is not built was the one program on the front page, described at specification level with a status note underneath saying so — while eleven programs that compile, run and are checked by cargo test were one paragraph further down.[x] is one, [~] is a half and what makes it a half is written in the box, [ ] is none. The method is stated so the number can be checked rather than believed: a percentage nobody can re-derive is a mood.nikaia describe (built this week), nikaia fmt, nikaia doc, the WebAssembly library (ADR-130) and the Python binding (ADR-131).[~], from their own text rather than from generosity: the standard library (it exists in the narrow sense the examples need) and the HTTP server’s box, whose D3, D4 and D5 — the runtime and the I/O layer — are built.scripts/check-roadmap-numbers.py, in CI beside the ADR citation check. That table is the one thing on the page nothing else would catch going stale: every box below it is a sentence somebody edits, and the number at the top is a sentence nobody re-derives. The script recounts and compares, names which area drifted and by how much, and fails the build.[ ] to [x] fails with the table says 21 of 26, the boxes say 22 of 26.What a parse fails with has a name — ParseError
(ADR-173), the owner’s answer to the
question 0.0.102 asked. And naming it made a miscompilation visible.
ParseError. A pub rule’s ledger entry wrote throws = ["?"] because a parse fails with a rendered string and a string is not a type — the last "?" written anywhere in this tree. The type is nikaia-std’s, in the prelude, keyed in std.contracts with no module in front, which is Overtaken’s shape: a program never writes a path to it, because it arrives in a catch.examples/inventory’s Stock::file and read go from ["?"] to ["ParseError"], and a function that opens a file and parses it is a sum of two named members (ADR-160 D1) instead of the boxed one — so a program can tell the file was not there from the file was not the shape the grammar says.throws fixpoint builds its graph from Item::Fn, and a grammar’s entry rule is not one — its contract is written straight into the ledger (ADR-082 D1) — so the lookup found nothing and the caller’s set came out empty. A callee with no node in the graph is read from the ledger now."?": a set of one "?" is the boxed channel and a box takes a String. The moment the caller’s set had a named member from elsewhere, the channel was inferred without the parse’s member in it — Result<i64, io::IoError> for a body whose ? yields a String, and io::IoError has no From<String> — so rustc refused the generated file, which is the one thing C.1 says may not happen. No program in the corpus had that shape, which is why nothing was red. The test that holds it runs rustc, because a ledger assertion would have passed before the fix too.NK2605 example said liest carries throws = ["?"] for a body whose one call is fs::read_to_string; it carries ["io::IoError"], measured by running it. And 7.1’s note on the catch diff still said not implemented … until error types are lowered, which 0.0.100 ended.rustc refuses the generated file with E0597, because the tail wrapper writes Ok({ … }?) and the block’s temporaries outlive the local. Found by this record’s own test; the same call bound to a name first compiles and runs, and examples/report/src/main.nika writes that shape, which is why the corpus is green. #139 carries it with its reproduction.open-decisions.md, and renumbering after a deletion turns each of those into a sentence pointing at somebody else’s entry — which is the failure that file already warns about, met from the other side. A gap in the numbers is cheaper to read than a citation that lies. Two such citations were fixed while closing this one.The last ["?"] is asked about, and a miscount of mine is corrected — the
second found by the first.
std.contracts carries exactly one throws = ["?"]. It carries none. The line that count found is in the file’s own header comment, which explains what entries wrote before ADR-158. Nine throws lines, seven io::IoError, two Overtaken, and not one ["?"] — the corrected number is the stronger claim, and a measurement that counted a comment is worth a sentence of warning. Fixed in ADR-101 §5, the index row and the test that repeated it.["?"] is still written is a public grammar rule’s ledger entry, because a parse fails with a rendered string and there is no type to name — #179, and that is the last one. Seven of the corpus’ eight mains carry the ["?", "io::IoError"] it produces, so naming it turns an open channel into a sum of two named members for all seven: a program could tell the file was not there from the file was not the shape the grammar says.offset, expected, message, found and a rule stack, and its render(source) makes the text a program prints today. The entry carries that measurement, Overtaken’s precedent for a std error type keyed without a module in front, and what each answer costs — and it does not guess the name, which says whose fault a rejected file is and is the owner’s sentence to write.A crate, a description, and a refusal — the chain three entries were waiting on, run from a program; and the order that said they were waiting, which had gone stale.
NK2501 fires end to end. A Rust crate with an Rc field, nikaia describe writing crosses = false from that field (ADR-123 D2), the description merged into the ledger every analysis reads (ADR-104 D1), and a spawn refused in this compiler’s words on the .nika line, with a way out and no word of rustc’s in it. Asserted from a program, because a hand-written ledger proves the last link and none of the others.NK2501 and NK2502 can fire, for the first time, on a described foreign type — and ADR-104 §5’s the day the describer reads fields, both reached from the same test.par_iter, the same shape for #182’s lazy walk — and a server is the program they wait for. What is left of the lock’s rules is D7’s stored lambda and nothing else.rustc’s own Send bound refuses against the right .nika line through ADR-005 D7’s translation — the position kept, the words rustc’s, which that record carries as its own open half.An error that newly reaches a catch is named once
(ADR-101) — NK2402, and the record said
it could not be built.
NK2402, a warning. A catch handles everything that reaches it and throws names no types at a signature, so the set arriving at a handler is open: it grows whenever a callee gains a failure. The handler is still a correct program and still handles the new error — as it handles everything. What was missing is not a refusal; it is that nobody was told.catch, because that is what the sentence is about: this call brings something new in here. The build compares the committed nikaia.contracts against the one it inferred, once per package, and the checker — which already knows at every failing call whether it is inside a guard and which callee it resolved to — says so. Nothing is walked twice for it.error, one that ignores it and one that dispatches with match error are told alike (D3), because the question is the same for all three.std.contracts writes throws = ["?"] on every entry, so there is no set to diff. ADR-158 gave std its error type: the file has nine throws lines now — seven io::IoError, two Overtaken, and exactly one ["?"] — and a program’s own ledger records named sets too, ["?", "io::IoError"] in examples/inventory. A set that gains a member is a change a diff can see, which is all D1 ever needed.--locked compares the ledger byte for byte, so a grown throws set was already a failure there and changed_lines already narrated which entry moved. What is new is the reason it is the right failure.The describer reads fields (ADR-123 D2) — the one line three entries were waiting on, and the last thing in the reviewed description that a hand had written.
crosses from a pub struct’s fields, which is the one claim in a description that a Rust signature could never make. Three answers and the third is the common one: false where a field holds an Rc or a raw pointer, true where every field is a scalar, a String, or one of those inside a container that changes nothing, and nothing where it cannot tell. Silence is nobody said, which is not permission (ADR-010 D1) and not a refusal either.examples/foreign-runtime/ is now the reviewed file entirely, comments aside. crosses = false on LocalHandle was the one line of it that a hand had written, and ADR-104 §5 said so: the day the describer reads fields is the day NK2501 has something to say.Rc, a raw pointer, a Cell. The first two are !Send; a Cell is not — it is !Sync. A Cell<T> may be moved to another thread exactly when its T may; what Rust forbids is looking at one from two threads at once. A draft that wrote crosses = false for a Cell would put a claim in the file that is false, and a reviewer would have to undo it — the opposite of what D5 asks a review to do. So Cell and RefCell are not on the list the describer reads, and the record carries the correction.true half is narrow for the same reason read the other way: a promise is what a refusal is withheld on. An Arc<T> is Send exactly when its T is Send and Sync — two questions a field reader does not have — so it falls to silence, as does a field of another crate’s type and a tuple struct, whose body is not read at all. Nobody looked is not it holds nothing.The hash rule holds (ADR-104 D5, on ADR-100 D3’s rule) — §5’s step 3, and the line the previous package left: the description records the crate source’s SHA-256 and nothing compares it.
NK2505: a described crate whose sources have moved since the description was reviewed. Until now a crate could change under a reviewed file and every analysis would go on reading the old answers — which is exactly what D3 calls believing a dependency against its own sources, the thing its second row exists to prevent.crosses read off a field, a signature that lies corrected. So the row becomes a refusal and the command, which is the same thing said to a person instead of to a build.[sources] recorded nothing; both answer nothing moved. Refusing on that absence is what ADR-169 D1 keeps NK2201 from doing, and here it would refuse every crate that comes from a registry (C.4).\-continuation inside a string literal had been collapsed into one long line with its indentation baked in, so NK1137, the missing-trait refusal, the new NK2701 walk message and two generated-Rust templates each carried eighteen spaces in the middle of a sentence. Found while writing this package’s own notes, by reading the rendered output rather than the source — the same way the sentence a reader meets is the only place this shows.path.nikaia describe <crate> writes the draft
(ADR-104 D2, D3) — the command NK2504
has been naming since the refusal was built, and §5’s step 2.
.rs under its src/, takes the pub fn and pub struct items out of the text, translates them by Part III 15.2’s table, and writes contracts/<crate>.contracts. An entry exists because a program asked for it (ADR-028 D5), so the draft is proportional to use rather than to the crate.Ledger::render’s own says do not edit by hand - it is regenerated on every build, which is exactly wrong here: a description is written once, reviewed like code, and hand-edited where a signature could not say what a field does. A file that told its reader not to make that edit would be telling them not to do the thing D5 asks of them.pub item inside a mod is read as the crate’s own, because the module path a caller writes is a thing only a real parser knows; and a type it cannot account for is ?, which is the absence of a claim and never a guess. The command names what it could not answer, because a draft that silently left it out would look complete.path dependency only, with the reason. A version dependency’s sources are in Cargo’s registry cache, under a layout ADR-002 D1 hands to Cargo and never resolves here — a describer that guessed at that cache’s shape would be resolving a version. The message says so and names D5’s hand-written file as what to do instead.NK2504 runs is handed out rather than copied: the refusal takes the crate word in front of a qualified name and the describer takes the name after it — one walk, read one segment further.examples/foreign-runtime/*/contracts/hyper_shim.contracts the file nikaia describe will write, produced the way a reviewer would check it. The test runs the command against the crate in the tree and asserts the draft against that file — entry for entry and hash for hash, for all three projects, with no network. Two of the reviewed lines are not in the draft and both are D5: crosses = false on LocalHandle, which is read from a field no signature names, and the comments.serve calls two functions, crossing two others, smuggled two more. The hand-written file has all four because one reviewer wrote it for all three.The eager walks of a pausing sequence, both halves of them (ADR-172 D5) — which 0.0.95 named as the next step and said needed two things rather than one.
collect, count, nth and join over io::lines(). Each hands back a value, so each is a loop around the step: Lines has them as inherent async fns, and the site awaits. The split is the rule rather than a list of names — what a walk hands back is what says whether it is eager.NK2701, the same code and the same rule a for over the same sequence gets. ADR-025 D1 already said a step that can fail fails the enclosing function and that nothing marks the call; a count is a written call, so the rule covered it and only the compiler did not. The message says a step of what count walks rather than a turn of this loop, because that is what a reader is looking at.collect is .collect::<Vec<_>>() below, because Rust’s needs to be told what to build and this emitter has no types. Lines::collect takes no type at all, so the turbofish is skipped where the site pauses — an Iterator::collect never does, which makes that the difference rather than the method’s name.map and filter over a pausing sequence hand back one whose steps pause, which is ADR-172 D3’s trait, deferred until a second producer needs one. They are refused from the lowering meanwhile, with the loop as the way out. #182 carries it, renamed again.io::lines().count() counted the failures as lines, and every walk of a
pausing sequence but the for is refused now
(ADR-172 D5).
Lines was an Iterator over Result[String, …], so count() counted the results — failures and lines alike — and the ledger said -> i64, so a program got a number with nothing anywhere saying it could be wrong. That is Part I 6.4’s bug class, in std. collect() was the same shape: a list of results, typed as a list of strings.for over io::lines() await its step took Lines’ Iterator away, and the question what happens to count() now is the one that turned up what count() had always been doing.for is the only walk of one this compiler writes, and every other is refused from the lowering, with the loop as the way out and the line under it. A refusal and no number, which is ADR-171 §4’s rule: a number is a promise the rule stays, and this is this compiler cannot build that yet rather than the language forbids it.keys(), chars(), drain() — is untouched, asserted rather than hoped for: a refusal that reached those would be this rule refusing correct programs (C.4).io::lines().count() still holds a thread where the for no longer does. It does not hold a thread; it did not compile after that package and gave a wrong answer before it. The entry as published stays and this says what is true.count over a pausing sequence is a loop around the step, but the step can fail as well as pause, and a walk of a failing sequence has to make the function around it throws — ADR-025 D1’s rule one construct over, written down nowhere. Half of that pair is exactly what the miscount above was. #182 carries it.A for may iterate something whose step pauses
(ADR-172) — the question
docs/open-decisions.md carried, answered A by the owner and built.
Seq says pauses, after it and beside sync and throws, and a for over one is written as a loop that awaits each step. The shape is ADR-025 D6’s: a word on a type that changes how the emitter writes a for’s step. That one answers can it fail; this one answers does it pause.sync already read as a step may pause and taking it as the claim would have been cheaper by one word. The two ways of being wrong are not symmetric: awaiting a step that has none does not compile, and what a reader meets is rustc about a file nobody wrote (C.1, C.4); not awaiting one that does costs a thread. So sync is a promise, pauses is a warning, and neither is nobody said — which is what a map’s step is, since it runs the lambda.std’s own, and the measurement is in the record so it is not asked twice: ADR-055 §5 already rejects tokio the runtime; tokio_stream::Stream is a re-export of futures_core::Stream, so the dependency actually in question is a trait definition and not a runtime; and what neither would shorten is the consumers, which is where the cost really lands. There is no trait today: of Seq[T]’s six producers exactly one can pause, so what the lowering wants is an inherent async fn next on one type.sync — a ledger that says both has said two things about one step, and reading it as either would be a claim the file does not make.fallible_loops’ arrangement for fallible_loops’ reason: the question is about a type, and the emitter has none. The two compose — a loop that pauses and can fail awaits its step and takes the ? inside.io::lines reads a chunk at a time. A hop to an I/O worker and back costs a wake-up, and paying one per line would have made this slower than the blocking reader it replaces; one per buffer is what it costs instead. One read per hop and not a loop to fill the buffer, because short is not the end of a stream: a pipe that produces a line a second stays a program that prints a line a second.for over this is the property this could most easily have lost.examples/tally/src/main.nika runs end to end, through a pipe: five lines, two blank, longest 31 characters, and the same file appended. That is the proof — the unit tests say where a line ends, and this says the program still works.map’s result — asserted rather than hoped for, because the other kind of mistake here is every program in the language.collect, count, nth, join, map and filter over a pausing sequence are Iterator’s below, so io::lines().count() still holds a thread where for line in io::lines() no longer does. #182 is that, renamed.A description’s entries reach the analyses (ADR-104 D1) — the first sentence of the record, and the thing the file it asks for exists to do.
contracts/<crate>.contracts was read to see whether it parsed, and the parsed ledger was dropped. Its only effect was silencing NK2504, the refusal that had asked for it — while that refusal’s own message promised the reader four answers in return for writing it: what may cross a thread, what the call may reach, whether it pauses, whether it can fail. It answered none of them, for two records.(a: i64, b: i64) -> i64 left fremd::zwei(1) unremarked, and removing the file turned NK2504 back on. That is the whole of what the file did.throws makes it a place that can fail, and sync makes it one a sync function may not make. The fourth is crosses, and it is not a gap here: a described call is not asked the crossing question at all, which is ADR-038 D7’s own wording and an open question on docs/open-decisions.md.std’s modules. The set of words that may stand in front of a :: is derived from the library ledger’s own key prefixes, so merging a description into it made hyper_shim look like std’s — and the first thing a program calling one was told was to write use std::hyper_shim. A crate is reached because the manifest declared it, not because a file imported it. Asserted, because it is the one thing the merge could break that turns a correct program into a refusal (Part III C.4).NK2504 is not a crossing, and the tally called it one: a value that may not cross a thread is a sentence about no part of a Rust crate nothing describes. The same rule every other split in that tally has — it must say what it counted.examples/foreign-runtime/ projects: each hands its value to a call the description names, so the crossing question is not asked of them and nothing there changes. That is the open question above, seen from the programs it is about.std wins a name collision, silently. A description’s names carry the crate word in front of them and std’s carry a module’s, so a manifest declaring a crate whose word is one of std’s modules would have two answers for one name. No manifest in this repository reaches it; the day one does, that is a refusal to write and not a silence to keep. #100 carries it as a suspicion, which is what an item with no reproduction is.A call that can reach a lock is refused under its own code — NK2503, which
ADR-039 D6 decided and
Part III C.6 wrote out. And the thing
that stood in front of it turned out to be a decision nothing built.
NK2503: a call into foreign code from which a lock is reachable through its arguments, transitively and through the fields of a struct. The refusal is about the call and not about a value crossing a thread, which is why it is a second code and not a second sentence, and the diagnostic is C.6’s to the letter: the call, the argument, the field that decided it, and a way out written as a line the program can be edited into — hyper_shim::render(state.counts.get()).NK2502’s and not a copy of it, which is what D6 asks for in so many words. One walk per argument, as before; what changed is that its verdict now carries why it refused, and the reason picks the code. Nothing is walked twice.NK25xx codes — a call that can reach a lock, not a value that may not cross a thread — for the reason every other split in that tally has: it must say what it counted.NK2503 is what found it. D1 says a Shared may not go into code nothing describes, for the sentence that was already the lock’s: which count a value gets is chosen per value (ADR-037 D7), so a Shared[Conn] is one shape for one value and another for the next in the same program, and no foreign signature can name both.Shared sat in contracts::send’s CHOSEN row and in its CONTAINERS row; the container row is answered first, so the row that would have refused was reached by nothing. Both rows carried a comment explaining why the name was there, and the two comments contradicted each other without either being read against the code.only_a_lock_is_refused_at_either_destination held that foreign("Shared[String]") is not refused — the opposite of D1 — and passed for two records’ worth of time. It is nothing_but_a_lock_or_a_count_is_refused_at_either_destination now, and the two families that are meant to refuse are named rather than skipped.Shared’s refusal had the lock’s sentence, because there was one sentence. There are three now — a lock, a shared count, and a described type that says crosses = false — and each says its own reason and its own way out. A refusal whose sentence is about a lock would be wrong about the other two, which the MayNot arm’s own note had said it would be.NK2502 example used a Locked[i32], which is NK2503’s now, and says the shared count instead.no_program_in_the_repository_has_a_crossing_refused is green with both refusals in: nothing in examples/, benches/, tests/samples/ or the three ADR-038 D7 projects hands a lock or a shared value to a call nothing describes. Part III C.4’s promise, for the one change in this package that could have broken it.Vec[Shared[i64]] into code nothing describes is refused too, and into a task of ours neither is.Two questions asked, and six more refusals given their line.
for iterate something whose step can pause? #182 says outright that what is left of it waits on a ruling about the for, not work, and docs/open-decisions.md carries it now with the lowering measured: for line in io::lines() becomes for line in io::lines().await — the call awaits and the step does not, because Lines::next is a BufRead read that blocks the thread it is on. At user_parallelism = yes that is a thread the pool could have had.NK2502 asks only of a call nothing describes (ADR-038 D7’s wording), and a test holds that silence on purpose. What refuses the described case is rustc’s Send bound — on the right .nika line, which is C.1’s rule kept, with rustc’s words, which ADR-005 D7 recorded as the open half.match over two error types, a grammar’s rule reached with a dot, a sum with no name, and every malformed string literal.emit::interpolation works on the text of a literal: it knows an { was never closed and cannot know where the string is, because it never saw a file. The caller is emitting a statement and knows exactly where, so the handover happens there — written once rather than at each call, which is what keeps where from being something each refusal has to remember.A refusal from the lowering names its line
(ADR-171) — half of the roadmap’s Phase 2
the compiler’s own anyhow errors, which are still text without a position.
NK… code gives all four, and these gave the first and the last. There are 33 of them in the emitter, and a reader with a file of any size was left searching.Flow::statement carries the byte the statement being emitted starts at, threaded for the type checker’s answers (ADR-028) — and it is the same number a caret wants.Refused carries it, refused_at! is refused! with the byte in front, and the line is rendered at the unit — the one place with both the byte and the path. The lowering knows the statement and not the file; whoever catches the error at the top knows neither, because a refusal travels as an anyhow::Error through a build that has moved on.overlap and select, the two jump refusals, and the spawn handed something other than a lambda. The rest are about an item, a literal or a whole unit, whose byte is a different walk.NK… number in front of them. A number is a promise the rule stays, and several of these are this compiler cannot build that yet rather than the language forbids it. Telling those two apart is the work a catalogue entry would rest on.An overlap keeps every failure (ADR-115
D1 and D2, built by ADR-170) — and
docs/open-decisions.md has nothing open.
overlap’s later failing branches join the winner’s list in written order. The first in written order is still the block’s (ADR-050 D5); what changed is that the rest are under it rather than gone. The block waits for every branch, so when combine<n> runs every outcome is known and the list is a fact rather than a race — which is the reason D2 gives for not cancelling the rest at the first failure.main that hands back an Err is printed by the language below through Debug, so that is the operator’s view of a program that stopped, and a short form there would be the one place the joined failures are dropped on the floor. Every line of a joined failure moves right, so the depth on the page is the depth in the list. The no trace note is said once, on the primary: it is about how this process was started, and repeating it under every failure says one thing three times and buries them.overlap that combines failures is the language doing something, so there is something to attach even where nothing was raised here — and the envelope it puts on still says no site recorded, which is D3’s own sentence.error.secondary as a value a program reads (which needs a Nikaia type for a list of errors before it is work), and whether the list survives a hop to a caller with a bare channel of its own.A type says what reading it touches, and I/O under a lock is refused
(ADR-169) — NK2201, the third of the
three things Part II 12.2 forbids, and the only one that had no code.
NK2201, and the question it waited on is answered. ADR-067 D1 split no I/O while holding locked data in two — what pauses is NK2202’s, what takes a lock is NK2203’s — and left is there I/O that does neither? as a question to answer before writing a code. Measured over std’s ledger: of the nine entries whose touch set names I/O, four are the printing functions, four are fs’s reads and its write, and one is neither. Reading a fs::Mapped is a page fault — a disk read with no call in the source at all, which neither suspends nor takes a lock — and inside an open door it is a disk read with the lock held.touches column on the type, in ADR-033’s own vocabulary. On the type rather than on a function because mapped[i] calls nothing a program wrote, which is the same shape iterates = "throws" already has on io::Lines (ADR-025 D6). A second such type is a line in a ledger and nothing in the compiler.NK21xx rules, for their stated reason: the tally has to say what it counted.NK2201 with a case that is not its own. The example was fs::write inside a door — which pauses, so ADR-067 D1 assigns it to NK2202, as the comment two lines above the block already said. The page shows the page fault now, and its status note says all three are built rather than that nothing raises this one.touches is claimed nothing about. Everywhere else this compiler reads the absence of a claim fail-closed (ADR-010 D1); here it may not, because what is built on the column is a refusal, and refusing on doubt refuses correct programs (Part III C.4). The polarity follows what is at stake, which is ADR-027 D2’s own arrangement one question over.examples/, in tests/ or in benches/ opens a door at all, so the refusal costs nothing now — which is the former backlog file’s own opening rule, and the reason to take it before a program exists that a mapping inside a lock is correct for.Two catalogued-and-unemitted codes measured rather than left as questions (#175). Docs only.
NK2503’s refusal is built; its number is not. ADR-039 D6 says the check is the crossing walk generalised, never copied — and it is. A lock reachable through a struct’s field, at a call into code nothing describes, is refused today, with the lock’s own sentence in the note and D3’s way out; a three-line program reaches it. What is left is a number and a sentence, not a walk: Part III C.6 writes NK2503’s own message — a refusal about the call — while the shipped one is NK2502’s, about the value. The work is one branch at the site that already walks every argument, and what it needs first is telling lock from count in the walk’s verdict, since Shared answers may not into foreign code as well and is not a lock.NK2201 has exactly one thing to be about. ADR-067 D1 split no I/O while holding locked data in two — what pauses is NK2202’s, what takes a lock is NK2203’s — and left whether any third thing exists as a question to answer before writing a code. Of the nine std entries whose touch set names I/O, four are the printing functions (sync, locks = true), four are fs’s reads and its write (not sync), and one is neither: fs::Mapped::deref. Reading a mapping is a page fault, a disk read that neither suspends nor takes a lock — so mapped[i] inside an open door is I/O while holding locked data and nothing says a word.examples/, in tests/ or in benches/ opens a door at all, so the refusal costs nothing now — which is §2’s own opening rule about refusals, and the reason to take it before a program exists that a mapping inside a lock is correct for.crates/nikaia/tests/tasks.rs was edited after cargo fmt --all had run, so cargo fmt --all --check — the first of the five gates — failed on main. The lesson is the gate’s own: it is the last thing to run before a commit, not the first, and an edit after it is an edit before nothing.The re-entrancy check is a build option now (ADR-039 D8, built by ADR-168) — and measuring it first found the note describing it to be wrong about two of its three clauses.
nikaia.toml has no key for this option. NK2203 refuses the nesting and has for some time; the runtime check is in std’s crossing shape, as a mark read before every acquisition. Only no key was true. A stale status note is a defect in its own right (README.md §1) — a reader cannot tell a plan from a promise — and this one told a reader that a guarantee they have is one they do not.reentrancy-check in [build], yes or no, yes by default. A third spelling is refused rather than guessed at — on reads like this option and is not it, and a build that took the default for a word it did not know would ship the guarantee the manifest declined. reentrancy_check with an underscore is the mistake [build]’s own key list exists to catch.std’s own tree. The first is ADR-037 D4’s rule for any switch; the second is what the other two switches did not need, because the check lives in the nikaia-std crate and the word decides what that crate contains. Without it the second project on a machine links the first one’s answer (ADR-021 D7: dimensions coexist; they do not share). The mechanism is a Cargo feature, and the generated manifest writes default-features = false for a package that declined it.RefCell’s own borrow flag, which notices a re-entrant borrow whatever anyone asks. It is the language below’s and no option of ours switches it off — more noticing than the switch promises, which costs a program obeying the rule nothing, and is stated rather than left to be discovered.nikaia-std’s own test does — no Nikaia program can get there.The shared types are the language’s, and a prelude name written with a module is refused (ADR-167) — Part I 1.3’s list seen from the two sides ADR-162 did not reach.
use and named nowhere on the list. ADR-162 D3’s test compares the page against what std keys bare, and it watches functions: a type is keyed under its own name — SharedMut::access — so Shared, SharedMut and Locked walked straight past it. Part I 6.2 and Part II 12.2 write let counter = SharedMut(0) and always have, against a list that ends and there are no others.NK1163: a name that needs no use, written with a std module in front of it. io::println("x") is the consistent guess — every other std name wants its module — and what it got was rustc saying cannot find function println in module io about a file nobody wrote. Both halves are required or it would be a guess: the module genuinely has no such name and the bare one is a name std keys, which is what separates it from a module nothing describes yet (ADR-140 D5).lock::SharedMut(0) — the shape every other std type wants — types as nothing, so Part II 12.3’s nesting refusal (NK2203, which ADR-039 D2 calls always) silently does not run, and the program reaches rustc. The same probe on io::println gave the second finding.check::HULLS, the list is_hull reads), which is the right side: what makes these three names a program writes with no import is that the checker knows them by name.Stdio::null(). Traced since — the wrapper’s trace now says which invocation is Cargo’s target-info probe — and in a clean sweep both probes are recognised and the whole workspace passes, 124 binaries, twice over. What the failing sweeps have in common is that they are the first run after a rebuild.lock::SharedMut(0) is still refused by nothing, and that is ADR-140 D5’s deliberate silence: lock is not a module std has, and use std::lock is accepted along with every other unknown module, because refusing on a surface that does not exist yet is Part III C.4’s correct program refused. NK1163 reaches a prefix that is a module; a prefix that is not stays where that record left it.Cargo’s target-info probe is given an empty standard input (ADR-166) — half of #201 #201, an entry that had been open for months and had already recorded its own best hypothesis as refuted. The hypothesis was right; the measurement that refuted it was measuring the wrong stream.
cargo test --workspace failed the project tests because of what was on the terminal’s input. Cargo asks every rustc wrapper what the target looks like with rustc - --print=… — - meaning the program is on standard input — and writes nothing there. Passed straight through, that standard input is whoever started the build, and anything sitting in it is read as a Rust program: failed to run rustc to learn about target-specific information, with a parse error at <anon>:1 about text nobody offered as source.- and asking a --print is handed /dev/null now. Neither half alone is the probe: - is what makes rustc read standard input at all, and a --print is what says the answer is not a compiled program — and a real compile names a file, so nothing that genuinely hands rustc a program this way loses one.cargo test … < /dev/null failed identically, three runs out of three. That measurement was right about what it measured and wrong about what it concluded — closing the outer command’s standard input does not close the test binary’s, and the nested build inherits from the harness.target_arch= instead of trying to compile it. Without the fix that test reproduces the original error exactly, which is what makes it a test rather than a description.cargo test -p nikaia --test project now passes on its own, repeatedly. Two things still show in a parallel sweep, and the entry carries both: the new end-to-end test fails inside a fully parallel -p nikaia run with its own contaminant file reaching rustc although the wrapper hands that invocation /dev/null — a smaller and much stranger claim than the entry ever made, and why that test is #[ignore]d rather than left to flake in CI; and --test project --test one_name together fail on Blocking waiting for file lock on package cache, which is contention over the one Cargo package cache those tests share on purpose.There is no postfix ?? (ADR-165),
and the refusal is where a reader meets the three spellings that replace it.
The owner’s answer to the one question
docs/open-decisions.md had carried.
a ?? b is binary: a value on the left, a fallback on the right. a?? is the same two characters with nothing on the right, and it would not be that operator with an argument missing — it would be an unrelated thing wearing its punctuation. One spelling, two unconnected meanings, told apart by what follows.a ?? b; a jump is a ?? throw NotFound or a ?? return r, since the four jumps are expressions (ADR-138 D1); and I know it is there is a ?? panic("…") (ADR-161 D3), which ends the program with the program’s own words. Before it, a reader who copied ADR-018 D3’s line got expected one value, or an expression in brackets and a list of tokens — true, and no help at all to someone who wrote the two characters on purpose (Part III C.2).a_head_parses_what_a_body_parses is there to catch.crates/nikaia/tests/coalescing.rs: the refusal in both positions, the chain ADR-066 D4 allows, and each of the three spellings compiled and run.lookup(a.query("id") ?? return bad_request()). A record is written once, so that one keeps its text and ADR-165 D2 carries the correction — the arrangement #191 named when it found the line. That entry is closed.??, so no program changes. What changes is what a reader is told who tries.docs/open-decisions.md has one entry left: where an error’s secondary list can live when the channel has no envelope.A joining block has one outcome, and a handler on it binds what the
branches threw (ADR-164) — which is
ADR-115’s first step, and where its
secondary list will live.
overlap { … } catch { … } did not lower, and it is ADR-115 D4’s own written example. A ? per branch leaves the function, so a catch on the block got a match over the vehicle’s tuple as though it were a Result. The branches become one outcome in std now — nikaia_std::task::combine<n>, taking them in written order and answering the first Err, which is ADR-050 D5 written as the language below’s own control flow. Where the failure does leave the function, the ? is one, on the block. ADR-163 D3 named this rather than fixing it, and said why: one function that sees every outcome is where ADR-115’s list goes, and writing the combination twice would be writing the same loop twice.throws at all in this shape, so its channel is the box — and match error { LoadError::Missing(w) => … }, Part I 7.1’s whole point, does not compile over one. A joining block has an error set of its own now: the union of its branches’, run through the same channel derivation a function’s key gets.catch { println(f"{error}") } over a named channel was unused_braces — the envelope was opened as a block wrapped around the handler, and is its first statement now. fn f() -> String throws { throw E() } was unreachable call — the tail of a throws body is wrapped in Ok(, and a throw writes its own Err(…) and leaves. A tail return is the other way round and stays wrapped: it is rewritten to its bare value there, and the Ok( is what makes that value the function’s outcome.select’s arms are the same rule in that construct’s shape: where the handler is on the block, the winning arm takes its own failure apart and its value becomes the Ok half, because a ? there would leave the function too.secondary list itself is not built. combine<n> drops every failure but the first, exactly as the ?s did — the tests say so on purpose.docs/open-decisions.md: ADR-115 D1 says every error carries the list, and two of the four channels have nowhere to put one — a library’s error travels bare (ADR-159 D2), and a generated sum’s members each keep their own. Those two are the common ones: a function that only reads files has io::IoError as its whole set, which is exactly that record’s three loads, two of them failing on the same outage.crates/nikaia/tests/joining.rs is eleven now, most of them compiling and running the result — including one that asserts rustc’s stderr is empty rather than that it succeeded, which is the only way to hold a warning about a generated file closed.A block that joins on the executor pauses, and its branches travel in the
function’s own channel (ADR-163) — two
correct programs rustc refused, in files nobody wrote.
overlap of branches that cannot pause left the function sync, and the vehicle’s .await landed in a fn that is not async (E0728). Ten lines were enough to reach it: two calls to fn twice(n: i64) -> i64 inside an overlap. Both constructs lower to an .await on a vehicle in nikaia_std::task whatever their branches do, so a body holding one parks on the executor and is not sync (ADR-055). select had it too, for the same reason one construct over.Ok::<_, Box<dyn Error>> whatever the channel was (E0277: ? couldn’t convert the error to IoError). The box was right while every channel was one; since ADR-157 a channel is a Thrown<E>, a library’s type bare (ADR-159 D2) or a generated sum (ADR-160 D1), and the ? under the block converts into whichever it is. The branch now names the string the signature wrote, carried on the flow beside the ledger key that was already there — two answers that have to agree are one answer or a bug.examples/, not benches/, not tests/samples/ — both are held by their own tests, and those tests happened to write the two shapes that still worked: an overlap over fs::read_to_string in a throws main whose channel was still the box, and a select in a test that only checks the refusal of one arm.async, the second the day the channel got a type. Neither record had a reason to look at a construct it did not change.sync over one is refused (NK2202), which is the other half and not a nicety: ADR-027 D4 says an assertion is never overwritten by the inference, so fixing the derivation alone would have left a hand-written sync on a body that pauses — and the language below would be the one to say so. A construct has no ledger entry, so the note says where the pause is instead of naming one that does not exist.throws deliberately does not ask the same question. A block’s failures are its branches’, which that walk already reaches by descending into them; treating the construct as opaque there would put a "?" in the set of every function that writes one — a claim about failures made to fix a fact about pausing.overlap { … } catch { … } does not lower, and that is ADR-115 D4’s own written example. It is named as a defect (#136) rather than fixed, because the shape that fixes it is that record’s own first step: handling at the block means looking at every outcome instead of returning at the first Err, which is exactly what its secondary list is. #199 now opens there.crates/nikaia/tests/joining.rs is seven tests, five of which compile and run the result — the only shape that holds Part III C.1 closed. One of them asserts that a second failing branch is nowhere, which is the line that changes the day #199 is built.The roadmap is brought level with what is built, and its tail is put in the order its own last section already states.
throws had been a set of error types since ADR-023 and every one of them lowered to the same opaque box, and all four shapes that set can have are built now; the map read as a T? (161); and the prelude’s list enforced in both directions (154, 162) with Bytes the language’s (156).std::db, the C library and the query DSL — and the bare-metal target sat after the HTTP server rather than before it.??. A map read is a T? and a ?? b over one is a T, so it has left that list.std.nika files that are implicitly imported, both of which have been answered by record. The shape is settled; what is left is surface, and the box says so.Diagnosis and safety are the language’s: eprint, access_all and
update_all join Part I 1.3’s list
(ADR-162) — and the list gains the test
that would have said so.
use and named nowhere on the page. ADR-154 §5 enforced the prelude’s list in the direction a program writes: a std name that lives in a module is refused without its prefix. The other direction was unwatched, and a name the compiler keys bare that the page does not list is one nobody ever meets, because nothing refuses it.eprintln / eprint are the same two on standard error. They are in the prelude rather than in a module — against a Part I list that wrote three of the four. The compiler followed Part III.eprint is diagnosis (D1), which is what panic and assert beside it on that list are for. And what it does is not one thing: on a hosted target a file descriptor, on WebAssembly a call into the host, on a machine with no operating system a serial line or nothing at all. A module cannot decide that, because a module does not know the target — the compiler does. Writing use std::io in front of it would say it is a library function like any other, which is the one thing it is not. This is ADR-161 D3’s argument one name over.access_all and update_all are safety (D2). They are Part II 12.3’s doors over several locks, and what they buy is that a deadlock cycle cannot form: every acquisition takes the locks in one order however the program wrote them (ADR-039 D2). They are free functions rather than methods only because a method has one receiver and these have several — and behind a use std::lock they would read as an alternative to nesting access calls, which is exactly the reading that makes the deadlock possible.std’s ledger keys bare is named on Part I 1.3’s list, read off the page rather than copied into the test. Taking eprint back out of the list makes it say reachable with no use and named nowhere.assert is on the list and missing. That asymmetry is the right way round: a listed name that is absent is one a program meets, and an unlisted name that is reachable is one nobody meets — so the second is the one that needs a test rather than a program.assert is the one name on the list that does not exist, and it belongs with the testing chapter (Part III 14). #190 is down from three names to one.docs/open-decisions.md loses its eprint entry, and has one question left.Reading a map through the brackets is a T?
(ADR-114, built by
ADR-161) — a key is data and may be
absent, so there is nothing there is an answer rather than the end of the
program.
m[k] on a map answers a T? and on a sequence a T; the write is unchanged and a compound one is refused with the written-out form (NK1162); the read is one call, nikaia_std::index::get, with an output type per container.panic(message) (D3), because it is ADR-114 D1’s own written way out for a key the program knows is present. It was on Part I 1.3’s list, is in Part III A.2’s list of unrecoverable errors, and did not exist — the form lowered to a call to a function nothing declares. It ends the program with the program’s own words, at the Nikaia line.a ?? b on a T? is a T (D7), which the checker claimed nothing about before.rustc messages this cost, and how each was foundNone of them would have been found by reading the emitted Rust.
?? could not join a view with a value (D2). A map read is an Option<&V> — copying what the map holds is never the compiler’s to do (ADR-008 D5) — while the fallback is written as the value it stands for. nikaia_std::index::or is where they meet, as two impls that do not overlap, so the emitter chooses nothing.&m[k] slipped past the checker (D4). A view of a T? answered unknown, which claimed nothing, so let s = &m[k] followed by s.min reached no NK1125. That was the shape both corpus lines were written in.m[k] = (m[k] ?? 0) + 1 — the form the refusal’s message hands a reader — had a &m inside the arguments of a set(&mut m, …). The value is bound before the write now, which is what a reader would write by hand.return xs[at] on a -> i64 is what said so: ADR-114 D4’s a T for a sequence cannot be a value in the language below, because reading one out by value moves out of the container. So the emitter writes (*get(…)) — a place for a sequence, and for a map a small wrapper whose Deref hands back the option. One trait still, and the caller still writes the same thing for both.io::Lines’ step threw the language below’s own error (D8). ADR-158 named every function in std and missed the one iterator. Nothing saw it while every program that read a line also had a "?" in its set; the day one had a named channel, the ? the loop’s step takes had a raw error on the left and a named one on the right.crates/nikaia/tests/map_reads.rs, six of which compile and run the program; examples.rs, which runs every example; and the ceiling in sequences.rs, which counts method calls nobody could answer and went from 39 to 40 the moment ?? stopped being typed for something.examples/1brc.nika and examples/access-log/src/main.nika now read … ?? panic(f"… was a key a moment ago"), which is ADR-114 D1’s own example: the key came from the map, and the line says so rather than relying on it silently.A set with two error types in it is a generated sum
(ADR-160) — the last shape a throws set
can have that had no channel.
enum per distinct set (D1), and not per function: two functions that fail the same way get the same type, so a ? between them converts nothing. The program never writes the name — a catch matches on the members’ variants (ADR-023 D4) — so the type exists only in the generated file, is defined once at the crate root and named crate::… everywhere. A module is a file of its own below, and a sum written into each of them would be a different type per file, with no way for a failure to cross a module boundary.throws it; a library’s bare, because no throw here raised it (ADR-159 D2). The sum adds nothing of its own — it is not where anything was raised, so its full() asks the member.match over a sum is taken apart by member (D3): one match per member with the arms that name its variants, and the source’s catch-all written into each, because the members are separate types and the language below has no fall-through. Every member gets an arm, including one the handler said nothing about. throw error inside a member’s arm puts the failure back in the variant it came out of.NK1151 for a match over a catch’s error with more than one type arriving (D4). The variants within one error type are closed; the set of error types is open, so such a match has covered no set at all — a callee that gains a failure sends a third type there. Refused twice, which is the arrangement NK1132 already has: the checker for the caret, and the lowering because it cannot write the file either way.rustc warning about the generated file. non_camel_case_types, on the sum’s deliberately-uncollidable name. A warning about that file is a defect here (Part III C.1), and sqlite3_from_end_to_end is the test that said so — which is also how the corpus turned out to reach the sum at all."?" anywhere: the opaque channel, which is the honest answer for something this compiler cannot name."?" left in the tree, measured: a grammar’s entry rule, whose failure is a rendered string and has no type. Seven of the corpus’ eight mains carry throws = ["?", "io::IoError"] for that reason alone. Naming it turns those seven into sets of two named members — the sum, already built — so a program could tell the file was not there from the file was not the shape the grammar says. #179 is now that entry.A library’s error type is a channel too, and it travels bare (ADR-159).
std failure crossing a function boundary. fn load(path) -> String throws { return fs::read_to_string(path) } declared the opaque channel, so a handler over load met expected Box<dyn Error>, found IoError — rustc speaking about the generated file, which is Part III C.1’s class. ADR-157 D1 named a channel only after a type this unit declares, which was right while nothing else could be named and stopped being right the moment ADR-158 gave std names.ConfigError::NotFound has two segments and the type is the first; io::IoError::NotFound has three and the type is the first two. Both the error-set derivation and the exemption that tells a variant constructor from a call took the first segment, so a throw io::IoError::NotFound(p) recorded io and the constructor beside it was taken for a callee nothing describes. Neither was reachable until an error type lived in a module.catch written directly over a std call has always bound what that call hands back, and since ADR-158 that is an io::IoError — so match error { io::IoError::NotFound(p) => … } over fs::read_to_string(path) catch { … } compiled and ran before this change, with no compiler edit at all. Measuring first is what kept the record to the half that was missing."?" in it, two members in it, or a method that implements a trait.Full for io::IoError and Overtaken (D3).throw in this program to have a site. ADR-157 D2’s envelope carries the place an error was raised (ADR-023 D6); a library’s failure arrives from a call, so an envelope would carry the place the program received it and read as though the program raised it.std hands the value back as it is, so propagating it is the plain ? the language below already writes. That is also what makes the two shapes agree from the program’s side: whether the failure came from a std call directly or through a function of its own, error is an io::IoError either way.error.full() says so (D3), in the words the opaque channel has always used for an error that came from below Nikaia: (raised below Nikaia; no site recorded). What it carries instead is what the failure was about, which is the more useful half for an environmental failure.throw io::IoError::NotFound(p) itself records no site. A program that wants its failures to carry where they were raised declares its own error type, which is what Part I 7.1 teaches first.examples/ lowers to. A function that reads a file declares Result<T, io::IoError> where it declared the box.std names what it throws, and the measurement is why the record is small
(ADR-158).
fn entries in std’s ledger, nine throw. Two already named their failure — Overtaken, on the lock’s two set(…; after:) doors — and work. Of the seven left, six fail with std::io::Error and the seventh with a boxed &str whose own doc calls it the same failure fs::read_to_string has. So it is one type, not a surface to design.IoError::NotFound, Part III 13.5’s example row writes throws = ["ConfigError", "IoError"], and Part III A.1 calls a missing file the example of a recoverable error.io::IoError (D1, D3): NotFound, PermissionDenied, NotText, Other, each carrying what the failure was about. Four, and short on purpose — ADR-023 D4 makes a type’s variants closed, so each is a lasting commitment: two are the specification’s own, one is the failure a program acts on differently rather than reports, and Other is D4’s named residue. Everything std::io::ErrorKind distinguishes and this does not is Other, which is the direction to be wrong in.std writes throws = ["?"] any more — the absence of a claim (ADR-024 D1) standing in for one. A test holds it, because the easiest way to lose this is one new entry written the old way.io and is written out (D2): use std::io and io::IoError::NotFound(p), which is ADR-154 D3 applied straight. The cost is named and accepted — a program that only passes the failure on writes nothing, one that takes it apart writes the second use. That is the same trade ADR-156 decided the other way for Bytes, and the difference is which half of the program meets the name: a result is written down by anyone who holds it, a failure only by someone who takes it apart.task::as_text takes the path, because its whole reason to exist is finishing a pair’s half exactly as read_to_string would have — and since D4 that includes saying which file.io::IoError, so every such function keeps the box exactly as before. The claim moves first and the representation follows, which is the order ADR-008’s analysis was built in for the same reason.catch is named once has a set to diff; NK2401 has something to check. None is built here, and #179 now states the gap as a measurement: every failing function in the corpus carries a named set and still travels in the box.The compile baseline records whether a block compiles, and no longer what
rustc called the failure — which [0.0.72] fixed one half of and this one
finishes.
tests/specification/COMPILES.txt is toolchain-independent. [0.0.72] took the order out of it, because which of a block’s errors is printed first is the printer’s business. CI then disagreed about which errors exist at all: Part I’s #61 and Part II’s #23 earn an E0282 on one stable and not on the next, because a later inference proves what an earlier one asked to be annotated.rust-toolchain.toml says channel = "stable" and ADR-001 D1 makes that the one source of truth for it. A diagnostic’s identity is therefore not a fact this tree holds still, and a baseline that records one fails for a change nobody made — which is the one way a baseline stops being read.compiles and refused say that and say it the same way everywhere.The failure channel is the error type, where the ledger names one
(ADR-157) — and Part I 7.1’s own catch
example becomes a program that runs.
match error { ConfigError::NotFound(p) => … } compiles. ADR-023 D1’s set has been in the ledger for a long time and ADR-013 D3 lowered every throws to one boxed error, so D4’s first row — the variants within one error type are closed, and a match over them is exhaustive — was unreachable through the channel. What that cost was the specification’s own program: rustc said expected Box<dyn Error>, found ConfigError about a file the author never wrote, which is Part III C.1’s class — and the page’s Status note said Implemented.Box<dyn Error + 'static>, so throw ConfigError::NotFound(path) — Part I 7.1’s own line, and what ADR-023 D10 says should happen — was borrowed data escapes outside of function (E0521), again about the generated file.throws set names a type and never a variant (D4). A variant with named fields is written as a struct literal, and the derivation recorded ConfigError::BadSyntax for it where the tuple form one line up recorded ConfigError. One error type, two entries — and a set of two is a set nothing can be named after, so the defect hid the other two.nikaia_std::error::Thrown[E], the envelope a named channel carries: the author’s value plus the site ADR-023 D6 says an error knows. A type of its own rather than a generic Raised, because Box<dyn Error> does not implement Error and one type cannot be bounded to cover both without the impls overlapping.Result<T, Thrown[E]> where the set has exactly one member and the member is a type this unit declares; the box everywhere else. The error type carries a lifetime exactly when it carries a view, spelled by the position — ADR-008 D9’s derivation, one position over.error, and what the language put around it — the site, and the trace if one was captured — into a Site beside it. So match error is the plain match the source wrote, f"{error}" is the author’s message, and error.full() is assembled from the two halves. A handler that never reads the error binds _error (ADR-090) and there is nothing to split.throw error passes the error on in the envelope it arrived in (D3) — the original site and the original trace, because a handler that passed an error on is not where it was raised."?" in the set is something this compiler cannot name, so there is no name to write. A set with two members needs the generated sum ADR-023 D1 implies, which is not built — a channel named after one of two error types would be a lie. A type another package declares is not this unit’s to name in a signature. A method that implements a trait answers with the channel the trait declares.std’s ledger writes throws = ["?"] throughout. That is also what makes the change safe: the tree does not go through the new path at all, and the programs that do are the specification’s. #179 now carries the two halves that are left — the generated sum, and std naming its errors.crates/nikaia/tests/error_types.rs, seven of which compile and run the program: a lowering that looks right and does not compile is exactly the defect this closes, so reading the Rust would not have caught it. It earned its keep immediately — the first shape of D2 kept the site and dropped the trace, so error.full() came out silently two lines where ADR-036 wants three, and only running it said so.A baseline that recorded which error rustc reports first, and therefore
disagreed with itself between two toolchains.
tests/specification/COMPILES.txt records every code a block earns, sorted, and not the first line rustc happened to print. what_lowers_is_handed_to_rustc_and_the_verdicts_are_the_recorded_ones has been failing on CI since it was written, on one block — Part I’s #68 — where the local toolchain prints E0433 first and CI’s prints E0425. The block earns E0308, E0425 and E0433, so both readings were true and neither was the fact.error: aborting due to 3 previous errors line is not a code. It is a count of the lines above it and says nothing a diff could be read from.rustc upgrade rewords its diagnostics and that must not be a failing test here. Which of two independent errors is printed first is a property of the compiler that printed them, exactly like the wording is.Bytes is the language’s, and the tether it needs is refused where a
program would reach for it (ADR-156).
Bytes exists (D1, D2). A name this compiler knows, written bare like Vec and String, lowered to one reference-counted, immutable run of bytes: handing a file on costs a count, never a copy. It reads as a run of bytes everywhere a &[u8] is taken, so nothing written against the language below has to know what it is.fs::read hands one back (D3), which Part III 17.1 and 17.2 have said all along and the ledger did not: it keyed Vec[u8].NK2303: a view of a buffer this body owns (D4). Where a function hands back a view that points into a buffer its own body made, the program is refused on the Nikaia line, with the buffer named and the two ways out — take the buffer as a parameter, or .to_owned(), which the compiler never inserts (ADR-008 D5).Bytes was never a missing name. Part III 17.2 says what it is: read returns Bytes, not a Vec[u8]: it is one shared buffer, and slices that outlive its scope are tethered to it. The first half of that sentence is a type; the second is Part I 6.6’s tether, which is ADR-008’s unbuilt state. Building the first and refusing where the second is needed is what makes both halves true at once.Bytes had been on Part I 1.3’s list since ADR-154 D1 and existed nowhere — not in std, not in the compiler, not in any .nika file.std’s, which is what enforcing the list forced a choice about: what needs no use is what std keys bare, so std’s would have meant bytes::Bytes behind an import. A type whose representation the compiler picks — which is what Part I 6.6’s three states are — is not a type a module owns.rustc used to speak about the generated file for this shape. A function handing back a view of a local buffer lowered to Rust that does not compile, and the diagnostic named a type in a file the author never wrote, which is exactly what Part III C.1 forbids.NK2303 fires only where the walk can name the buffer’s type and the returned expression is derived from the local holding it; a call no ledger describes raises nothing.views column answers Borrowed the moment a parameter carries a view, because a signature is what it reads. The refusal reads the body: a function taking a &str and handing back a view of a String it made tethers all the same, and that is the case the column’s polarity would miss.the_corpus_needs_no_tether holds that over every .nika file in the repository, which is ADR-008 §3’s free case held to rather than asserted.views column. #196 carries it, and its first item — a buffer to tether to — is now struck.Mapped does not deref to Bytes (D6). Part III 17.2 promises it; fs::map hands back a mapping whose deref is &str, which is what examples/1brc.nika parses. It is the tether’s own work.eprint rode with the Bytes entry in docs/open-decisions.md and is now an entry of its own: it is keyed bare and not on D1’s list, which joined by being needed once — the direction D4 of that record was written against.The tether’s analysis, built alone (ADR-008 D2, D7, D6) — the state is solved, written down and read by nothing.
views: ["data: borrowed", "<result>: tethered"]. A parameter is lent for the call, so a view in one borrows; a result borrows where a receiver or a parameter carries a view, because that buffer is the caller’s, and tethers where the buffer is one the body made.nikaia --tethers, beside --trust, --overlaps and --sharing. That is D6’s own inverse tool: the inverse tool is inspection, not assertion — @borrowed forbids a transition and this shows what was solved without being asked, so a change in representation is a ledger diff in review rather than a surprise in a profile..to_owned() written by the program, and Tethered is not built. So this change alters no lowering. An analysis whose answers nothing depends on can be held against the whole corpus and read, and being wrong costs a wrong line in a file rather than a wrong program..to_owned() hands back a String, which is not a view at all, so no view position is ever Owned and the lattice solved over is the two states below it. The top exists for the program to reach.examples/ and benches/ solves to Borrowed — ADR-008 §3’s worked check, read off the analysis rather than asserted. A test holds it as a ceiling, so a program that starts needing the state that is not built says so.fn name() -> &str { "Ada" } — or at a buffer this body made. Does this body own a buffer is the whole of it, and it is read off the ledger: a let whose initialiser hands back a String, a Bytes, a Mapped or a run of u8, plus to_owned and to_string by name.input is a position the analysis does not reach, because a pub rule’s entry is written by the ledger rather than declared as a function. That is #140’s remaining column, read from the other end.The tether, measured — and the one part of it that was a defect rather than a project (ADR-008 D9).
'static where a function has nothing to borrow from, because there is nothing for Rust’s elision to take. It asked that question of the written type, so -> Vec[Entry] — where Entry holds a &str — went on eliding and the answer was Vec<Entry<'_>>: missing lifetime specifier about a file nobody wrote, which is the defect D9 exists to close, one type in. The declaration is what tells the two apart and the emitter has it.'static written, rustc refuses the body instead — cannot return value referencing local variable text — on the Nikaia line, through --explain. That is D9’s own argument for why the widening is safe in both directions, now measured for this case too.impl that declares 'a the result is the subject’s, and 'static there is a promise the impl cannot keep — examples/1brc.nika’s anonymous constructor has no parameters and no receiver, so the widening reached it and the backend answered lifetime may not live long enough.NK2302 for a stored view parameter, the backend on the Nikaia line for a view of a local that escapes, which is D5’s residual hard error standing in for the missing state.views.rs’s 919 lines answer a far cruder question), three layouts per struct chosen per site, the buffer table, the ledger’s state column, the retention lint and the cleanup it moves, and @borrowed — which is vacuous until the rest exists, because it forbids a transition there is none of. It is at least four change packages, and the analysis is the one that decides whether the rest is worth starting.The second question goes where questions go: does a T? have a postfix
unwrap?
lookup(a.query("id")??), Part I 3.5 defines ?? as null coalescing and nothing else, and the entry’s own next line was what it needs is a decision before any work. It is in docs/open-decisions.md now, in that page’s shape, which is the rule the issue tracker’s own header states.a ?? b, a jump is a ?? throw NotFound since ADR-138 D1 made the four jumps expressions, and reaching a member is a?.b. What is left over is abort where it is null — the one thing Part I 2.3 exists to make a program say out loud, and a postfix ?? would be a one-character way to take that back.query twice and then unwraps; what the language has today is one binding, let id = a.query("id") ?? return http::Response(status: 400, body: "id is required"). Compiled and run before it was written down, not sketched.A grammar’s entry earns its columns (ADR-082) — #140, down from four missing columns to one, and the one that is left turns out to be a different question.
touches and locks are derived over a pub rule’s action blocks and folded into the entry the way a function’s are. A rule’s actions are ordinary Nikaia, walked by the same two passes that read a function’s body. examples/inventory’s Stock::file carries touches = [] now, and read — whose body is one Stock::file(data) — carries it too and has lost its locks = "?". That is the entry’s own reproduction, read backwards.pub rule has the entry’s key while its body is checked, which is what the derivation needed first: the checker files a call’s answers under the enclosing function, and an action has none, so a rule’s answers landed nowhere at all. What the key must not do is make an action a function, and NK2605 says that where it belongs instead — an action’s failure leaves the parser rather than travelling to a caller.crates/nikaia/tests/sequences.rs keeps had never seen them. It rises from 33 to 39 with that sentence on it. What a pattern binds has no type in this compiler — that is the parser backend’s — so a method call on one is unanswerable by construction, and an entry derived from such an action says undecided rather than nothing.keeps, which is the tether’s question rather than this record’s. A parse hands back views into its input — Stock’s Entry holds &str — so an entry keeps its input, and that is ADR-008’s tether rather than anything an action block says. Absent is the safe reading meanwhile: nobody said makes a caller hand the input over whole, where a derived keeps = [] would lend it to a parser that tethers views into it. It waits on the same mechanism docs/open-decisions.md’s Bytes entry waits on.The most negative i64 has a spelling
(ADR-136) — the completeness item
#135 carried, closed the smaller of the two ways it named.
-9223372036854775808 is a program now. It is i64::MIN and belongs to the type; as a negation of a positive literal its digits are 9223372036854775808, which no i64 holds, so the parser refused a number that is in the language. A - written directly against the digits is part of the literal, and the sign goes into the text the radix parser reads rather than being applied to what came out — so -0xFF is -255 for the same reason 0xFF is 255.- 5 and -x are the unary operator they always were; a binary - is matched by the rule that wrote it rather than by its operand’s, so a - 5 and a -5 are unchanged; and a . or an exponent after the digits hands the - back, so -1.16e+00 is a float and -2..<2 is a range.examples/n-body/src/main.nika is what caught the float. The first version of the rule took the -1 out of -1.16032004402742839e+00 and left the rest stranded — a misparse of exactly the class ADR-136 was written to remove, one form along. Six tests hold the boundary now.Bytes is a question, and it goes where questions go
(docs/open-decisions.md) — with the measurement in
front of it, which turned out to be larger than the name.
Bytes is not a missing name; it is the surface of an unbuilt mechanism. Part III 17.2 already says what it is: read returns Bytes, not a Vec[u8]: it is one shared buffer, and slices that outlive its scope are tethered to it, and Mapped derefs to it. So Bytes is the container Part I 6.6’s tether needs — and the tether is that section’s unbuilt half: the three states a view can be in are ADR-008’s, and what exists is the refusals rather than the reference-counted buffer.use is now exactly what std’s ledger keys bare, so std’s or the language’s are no longer interchangeable: one of the two answers puts Bytes behind a use, which is the opposite of being on D1’s list.fs::read hands back Bytes where the compiler hands back a Vec[u8], and a Status note said fs::map and Bytes exist.docs/open-decisions.md is not empty any more. The entry is in the shape that page asks for: what is blocked, three options, a recommendation — not yet, and the language’s when the tether is built — and what each direction costs if it is wrong. eprint rides with it, because it is the same sentence being revisited.docs/README.md §1) whatever the answer turns out to be.assert and panic, and says what they actually wait on: the testing chapter (Part III 14), which is not built either. Neither is a piece of work on its own.Bytes, so nothing is blocked by the answer today — what the question protects against is the second spelling arriving later, which is the problem ADR-140 spent a whole record removing.The prelude’s type half
(ADR-154 D3) — which is the decision the
record is named for: HashMap is the first thing outside the list.
std module is written with it, and NK1135 says so with the two lines to write: HashMap is written without its module, help: use std::collections, and collections::HashMap here. HashMap, HashSet, BTreeMap and BTreeSet became collections::…; CStr, Duration, Sender and Receiver went into the modules they live in.collections::HashMap::len, which is the shape fs::Mapped::deref already had.CStr all read the last segment, through one function that says why.http::Request is the ledger’s key and Request is what the files of http write, because they share one namespace. Only std’s side dropped the last-segment reading.NK1156’s help names the module for a type that has one and says drop the line only for a type on the list.:. &collections::HashMap[$K, $V] was read as a parameter named collections whose type was :HashMap[$K, $V], so every call on a map met NK1101 about an argument count nobody had written. The split now steps over a ::.channel::bounded and confirmed here: a parameter list that ended at the last ) in the text, which is the result’s where a result has parentheses in it.Bytes, assert and panic are on Part I 1.3’s list and do not exist. A name on a prelude’s list that does not exist is the one direction it can be wrong in without anybody noticing, because nothing reaches it.eprint is in the compiler and not in D1’s text. It joined by being needed, which is the direction D4 was written against.TaskHandle and the hulls stay bare, and that is on purpose: Shared, SharedMut and Locked are the language’s rather than a module’s, and a program has no reason to write TaskHandle.The prelude is a written list, small and closed (ADR-154) — the function half of it, which is the half that had a mechanism waiting for it.
use by reading one page rather than a file of the compiler’s. The rule it is built from is written beside it: nothing in it does I/O but printing, and nothing in it pauses.NK1117 with the line to add. A name that lives in a std module written without its prefix — std has text::digit_value, help: write use std::text, and text::digit_value(…) here. And a module used before it is introduced — fs is used here and introduced nowhere, which is Part I 9.1’s D4 for std rather than for a package.collections, text and foreign, because the generated Rust now writes the prefix the source wrote. std::collections is a module of nikaia-std and not a re-export, because one name in it is ours: which hash a map gets follows the provenance of the program’s input.collections::HashMap works end to end — the annotation, the constructor and the trusted-hash swap, which now reads the last segment so the type and the value cannot disagree about which map it is.std’s entries that live in a module are keyed module::name, and the ones that need no use — print, println, eprint, eprintln — are keyed bare. So the prelude is the bare keys, and adding a name to it is exactly what D4 says: a record, and a line in std.contracts keyed without a module. No second list in the compiler to drift from the page.Ledger::lookup matched on the last segment, which made the question unanswerable. digit_value and text::digit_value were one name to it. It matches exactly now — and that closed a defect #212 was carrying: a program with its own fn read found io::read, in a unit that does not carry the ledger of the package beside it, and the throws column then spoke for a callee nobody had resolved.sleep moved into std::time by D2’s own rule. The rule the list is built from is that nothing in it pauses, and that was the one entry that did. Part II 12.4’s line is time::sleep(5.seconds()).fs::, io::, cli:: and html:: in examples/ to need a use line it never had; the .nika corpus already wrote them almost everywhere. What it came to was two files, fifty-four inline sources in the test suite and six blocks of the specification.NK1156’s help names the module where the type has one — use std::fs and fs::Mapped — instead of telling a reader to drop a line they will need.HashMap, Duration and CStr are keyed without a module, so the rule cannot reach them and the bare spelling is still accepted. D3’s spelling works; what is missing is the refusal, which wants those three re-keyed the way fs::Mapped already is.Bytes, assert and panic are on the list and do not exist.eprint is in the compiler and not in D1’s text. It joined by being needed, which is the direction D4 was written against.A channel is std’s, and only bounded
(ADR-149) — which takes the unspecified
mark off Part II 12.5 and leaves ADR-141
D2 with nothing in that chapter still marked.
std::channel, with bounded(n) and nothing else. Four entries and no syntax: two values, two methods, and the tuple let that binds them was already built. A capacity below one is refused with its reason rather than quietly turned into something else — bounded(0) is a rendezvous channel, which is a different promise.send pauses where the channel is full and recv where it is empty, each leaving the waker the other rings. Back-pressure is a pause, and a pause is something the ledger already talks about: send carries no sync, so a sync body cannot send and the compiler names the promise in the way — without a rule about channels.recv hands back a T?, and null means every sender is gone. Not a failure: a closed channel is the ordinary end of a stream, and ?? is what reads it.Sender and Receiver joined the crossing analysis’s containers, which is the whole of the value type must cross, checked where tx moves into a spawn: the analysis already asks that question at a move.crates/nikaia/tests/channels.rs: nine tests, four of which build a program and run it, including Part II 12.5’s own example.Signature::parse took rfind(')') for the end of the parameter list, and channel::bounded is the first entry in std to hand back a tuple — so the last ) was the result’s, and the parameter list became everything up to it. Silently: a garbage parameter list still parses, and what it cost was every argument to the call being lent, because a parameter whose type is not known is one that moves.?? 0 needed an annotation nobody could write. A ??’s fallback is lowered with an .into(), which is right for ?? "none" on a String? and wrong for a number: Part I 2.4 already says a number literal takes the type its use asks for. Where the option’s own type was still open the conversion had nothing to resolve from, and the reader got “type annotations needed” about a file nobody wrote. A number fallback is now written as itself — sign included, since -1.into() in the language below is -(1.into()).NK2202 could not see a method call, so a sync body that paused was not refused. contracts::sync resolves a free call by name and stops at a method, on the ground that only a type checker knows what tx.send(1) goes to. So a function declaring sync and calling a pausing method lowered to an ordinary fn with an .await in its body, which is not Rust. The rule is now raised from the type checker too — the same code, the same three lines, from the only place that can ask it. Reachable before this record through TaskHandle::join.NK22xx table says where each half of NK2202 is asked.select over a receiver, a for over a receiver (which wants a Seq entry) and a rendezvous channel — all three ADR-149 §4’s, all three untouched.select { … } keeps the first arm to finish, and a task handle has cancel()
(ADR-148) — which takes the last
unspecified mark off Part II 12.4 and completes the pair overlap was half
of.
select is a keyword and a construct, in the same change. ADR-051 D3’s one ground that holds — reserved for a construct rather than against the possibility of one. The number that allowed it is zero: nothing in examples/, in tests/ or in the three pages wrote it as a name.pattern = expr => { … }, with _ and a binding name, and it lowers to a match over which arm won: std gains race2..race8 and a Race<n> per arity beside the overlap<n> it already had. An arm binding a name and then running a block is a match arm in the language below, so nothing had to be invented for it — which is also why the generated Rust reads as the source does, Race2::Second(_) and not Race2::B(_).handle.cancel(), over a flag the task reads at its pause point and the waker that makes it notice now rather than when whatever it was waiting for answers. It takes the handle, the way join does.crates/nikaia/tests/racing.rs: twelve tests, seven of which build a program and run it — including Part II 12.4’s own example, with a timeout short enough that a test does not wait five seconds for it.cancel() taking the handle answers a question the record left open. ADR-148 §4 asked whether a cancelled task can be observed to have been cancelled. It cannot: after the call there is no handle to ask with. That also means a cancelled task is never joined, so nothing waits for a value that is not coming.cleanup that pauses is adopted by the runtime and bounded by the cleanup-deadline (ADR-006 D3). One mechanism, which is the whole argument of D3.Ok so the vehicle sees one shape, and the ? is the first line of the winning arm’s body. overlap’s D5 puts its ?s on the tuple, because there every branch has a value.primary_expr was at the last one, so select needed room. Putting overlap and select together in a rule of their own took the word overlap out of six recorded messages’ “also possible here” lists. What was merged instead is the tuple and the grouping — both begin with a (, so the list is the same either way. tests/errors/EXPECTED.txt is what said so.select with a default arm (if nothing is ready yet), which is a poll and not a race and which no page writes, and cancellation of anything that is not a task — both ADR-148 §4’s, both untouched.Three sweep failures that are not failures, measured once and written down.
nikaia --input over every .nika file in the tree reports three refusals, and all three are the loop’s method. examples/hello-http/src/main.nika and examples/fortunes.nika write use http, and a package reached by name is declared in [dependencies] — which lives in nikaia.toml, which --input does not read. nikaia build inside examples/hello-http compiles it and its dependency and finishes clean. examples/inventory/src/page.nika is one file of a package whose Entry is declared beside it, so compiled alone it is NK1135 doing its job.fortunes.nika is neither of those. It is written at specification level, and examples/README.md lists its gaps: G6, the runtime binding that lets a handler see the request (ADR-018), and the postgres block — the database driver, which is itself blocked. Its render lowers and runs today.A span of time, and the call that waits one out
(ADR-150) — which takes half of Part II
12.4’s unspecified mark off the page and leaves select the other half.
5.seconds(), and four names beside it. seconds, millis, micros, minutes and hours, on either integer type — ten ledger entries and not five, because 5 is an i32 where nothing asks otherwise and a count that came from a length is an i64, so a file holding both needs both halves or one of the two is refused in the language below’s words about a file nobody wrote.Duration, re-exported rather than wrapped. A wrapper would be a second type with the same meaning and nothing to say that the first one does not. It carries crosses = true, which is an easy claim to review: a span is a number of ticks.sleep, and it is a suspension point. The thread is given up rather than held, so a task started before the wait has its value by the time the wait is over — on the one thread user_parallelism = no gives a program, which is the test that proves it.Pending in this runtime waits for a completion, and the park is how the thread waits for one; a sleep waits for nothing a worker posts. So a sleeping future leaves the time it wants in rt::timer and the park takes the nearest of them as its bound. One cell and not a queue: every pending future is polled once a round and asks again, so the cell is rebuilt each round rather than kept in step with registrations nobody would remove from.crates/nikaia/tests/duration.rs: nine tests, four of which build a program and run it.keeps::moves names what copies, and a type it does not name is lent — so the first program to write sleep(50.millis()) got a & in front of its argument. The same defect ADR-147 D2 found one type over, met again because the copy list is a list.std name is keyed, decided by measurement. A bare call takes its .await from an exact lookup, so sleep’s entry is sleep — as print and println already were. Resolving a bare name by its last segment instead is the emitter guessing: a unit built from --input does not carry the ledger of the package beside it, so examples/inventory’s own read found fs::read and took an .await for a function that is not a future. The example’s committed lowering said so.can_fail does resolve a bare name into std, and the one call in the corpus that would show it sits inside a catch.5.seconds() and sleep are built, select is not.Duration::seconds(5), which D2 names beside the extension for a computed count. No program has asked, and ADR-028 D5 is the rule that an entry exists because one did — count.seconds() is what a computed count is already written as.What blocks the database driver, written down where the next reader looks (ADR-143) — because a measurement nobody recorded is a measurement somebody takes again.
meta::column and the row type derived from a grammar. For the compiler to know which columns a grammar declares, the grammar has to run while the program is built — and #178’s own rule for that says it may only be done by compiling the generated parser and running it, never by interpreting the grammar a second time. So step 1 waits on an entry four pages above it, and nothing in the record pointed there.grammar! { … } for a grammar item, and sysroot.rs already knows where winnow_grammar and winnow live. What is missing is the harness — a crate holding one grammar and a main that parses the block’s bytes, built and run during the build, keyed in the cache on the grammar’s source — and a way for what it found to come back. Today a dsl block’s holes come from a scan of the body text (crates/nikaia/src/dsl.rs), which that file’s own note calls an approximation, and dsl html { … } is the one block this compiler runs at all.comptime CONFIG = Config.value(from "config.toml") needs back is an arbitrary value. The two share a harness and part ways at the return, which is worth knowing before either is started.sqlite3 from end to end
(ADR-147 §5 step 5) — which completes that
record and finds three things four libc calls could not.
examples/sqlite/main.nika: it opens a database, prepares a statement, steps it and reads a row back — hello, C. Everything the C boundary offers is in it and nothing else is: a buffer lent for the call, an opaque handle with its cleanup, a handle that may be absent filled through an out-parameter, and text the library owns copied once by std. What closes the database and the statement is written nowhere.libsqlite3. A gate that depends on a library not every machine has is a gate people learn to ignore — and what proves the decisions is libc, on any machine, in the four tests beside it. What sqlite3 adds is a library’s surface rather than four calls.sqlite3_prepare_v2 takes the database the out-parameter filled, and a sqlite3? cannot be handed to a parameter that takes a sqlite3: ?? wants a fallback and there is no second handle, ?. reaches a member and this is a free call. The language turned out to have the answer already — slot ?? throw Refused::NoDatabase narrows a T? to a T, which is ADR-138 D1’s jump-as-an-expression meeting ADR-089’s ??. Nothing had ever written the two together, and no shorter program would have.pub struct sqlite3 is not the shape Rust’s own non_camel_case_types expects — Part III C.1’s class, invisible until a library with a lower-case type name was called.NK1161: throw "no database" read like a program and was not one. Part I 7.1 says what is thrown implements Error, and the impl line says so, and text does not — so rustc answered the trait bound str: Error is not satisfied about a file nobody wrote. It had been writable since throw existed and no program in the tree had written one.NK1154’s own machinery one construct over: a bare 3 fits every numeric type and arrives as ? (Part I 2.4), so the kind is the part of a literal that is known without one. Only Part I 2.2’s own types — a type this file declares may have its impl Error in another file of the same package, a package’s type is not this compiler’s to answer for, and a caught error re-thrown is a ? (C.4).A handle may be absent, and T? is how it says so
(ADR-155) — the question
ADR-147 D3 left open, answered and built
in the same round.
fopen returns NULL where it cannot open the file, getenv where the name is not set, and most of C’s handle-returning surface does — and nothing in a declaration could say it. ?? and ?. are how a program gets past it; there is no second spelling and no rule of its own.T? is the absence of one — the two states C spells with a pointer and NULL — so Rust lays Option<T> over the same machine word: a nullable handle is one word, and &mut sqlite3? is sqlite3 ** exactly as C writes it. The out-parameter works rather than merely typechecking, which at this boundary is the only kind of working there is.? is a claim, and it is checked (D3). C may still hand back nothing; what the program gets then is an abort naming the declaration rather than a handle that is secretly null. The claim is the author’s and the check is the compiler’s — the arrangement sync on a declaration already has.CStr::to_string lost a failure (D4). It reported a null address and bad UTF-8 as one throws, which conflated there is no text with the bytes are not text. The first is a value now, and what is left is the failure fs::read_to_string already has.NULL arriving in one is undefined before any check could run. So a declaration hands back the address C returned and the hull goes on at the call — from_c checks the claim, maybe reads NULL as None.D5, and it was written because the code found it. &mut Block? parsed and the ? went to the view, which is Part I 2.3’s own reading and the wrong one here: what C wants is a slot that holds a handle or nothing. A view at this boundary lives for the call and is never absent, so the other reading is a shape nothing writes. Turned around for &mut T and &[T] only, in both directions of the ledger’s type text; a plain &T? is untouched.
getenv both ways, and one runs posix_memalign’s out-parameter.sqlite3 end to end. Nothing is in its way any more. It is not what proves the four decisions — libc does, on any machine, and four of the twenty-nine tests compile against it and run. What sqlite3 adds is a real library’s surface rather than a libc call each.Text a C library hands back is a CStr that std copies
(ADR-147 D4) — which completes that
record’s five decisions.
fn getenv(name: &[u8]) -> CStr, and raw.to_string(). getenv hands back memory the caller does not own, whose lifetime is the library’s, and which ends at a zero byte rather than carrying a length. None of those three is something a String can be made of without reading it — so the reading is std’s, written once, inside unsafe, where every program would otherwise write the same loop.CStr is an opaque handle with no cleanup, which is what tells it from D3’s: a FILE is the program’s to close and a C string is not the program’s at all, so std::CStr frees nothing.throws rather than an abort, because a caller can do something about either — getenv finding nothing is an ordinary Tuesday, and Part III A.2’s aborts are for what a program’s own arithmetic got wrong.rustc against libc and run it: strlen and abs for D1, fopen/fclose/fileno for D3, getenv for D4. libc is what proves the four, not sqlite3 — it is on every machine, and the record’s step 5 is a test rather than a decision. Part III 15.1’s third block is a program too, so the specification’s lowering floor went up again, 57 to 58.sqlite3_open(path, db), and a handle has nothing to be before the call. That is in open-decisions.md with three ways out and a recommendation. What is built meanwhile is every library whose constructor hands its handle back, which is fopen’s shape and most of C’s.A C library’s handle is an opaque type … released by …
(ADR-147 D3).
opaque type FILE released by fclose in an extern "C" block. An opaque type is an address the language never dereferences: it is moved and stored like any value, and its release is a cleanup the compiler runs at the end of its scope (Part I 6.4), so a handle cannot be forgotten. That is the shape every C library with a session in it is made of — a database connection, an HTTP client, a compressor — and none of them was writable before.#[repr(transparent)] over the address, and Drop for the cleanup. The layout is the whole point: a handle is the address, so &mut T at the boundary is T** the way C writes it and a handle passed by value is the pointer. A newtype Rust may lay out as it likes would be a different program at the boundary.fileno(f) reads the handle and f is still the caller’s to close, so the address goes by value and the value stays here; fclose(f) is the cleanup, so the handle goes with it and Rust’s own move keeps it from being released twice. The & ADR-094 D1 would otherwise write is the wrong address as well as the wrong ownership — &FILE is FILE** where C wants FILE*, the same mistake Rust’s fat &[T] would have been in a declaration.NK1160 covers all three ways of reaching past a handle: a field, an index, and a call — because an opaque handle has no constructor. There is no value of one this language can make, so a constructor would have to invent an address, and an address somebody chose is the one thing a handle may never be.opaque, type, released and by is reserved. The grammar is scannerless, so a word means something only where a rule asks for it, and all four stay names everywhere else. Reserving one buys exactly the sentence a reader who writes it gets (ADR-117 D2), and this position says that sentence without taking the word away.rustc against libc’s fopen, fclose and fileno and runs it — no library the machine may not have. Part III 15.1’s second block is a program too, so the specification’s lowering floor went up, 56 to 57, and two more of its blocks compile below.sqlite3_open(path, db) needs db to exist before the call; C writes an uninitialised pointer, this language has no uninitialised binding, and D3 gives a handle no constructor — deliberately. NK1160 says so rather than letting rustc answer. The question is in open-decisions.md with three ways out and a recommendation — the nullable, because NULL is what C means there and it also answers the failure every handle-returning function has. What is built meanwhile is every library whose constructor hands its handle back, which is fopen’s shape and most of C’s.CStr is the record’s step 4 and is next.the former backlog file holds only what is open.
NK2501 and NK2502 are in the list of codes the compiler emits, send.rs asserts one firing, and the question the entry pointed at in open-decisions.md was answered by ADR-123. The ring’s park hears the bell was built except for a remainder that is another entry’s. A C declaration cannot name a pointer is ADR-147’s, answered two packages ago. And the ring’s block_on test flakes named contention as the mechanism; the real one was four instances of one mistake and was fixed in 0.0.43, since when the test has been green in every run.std refuses, and #201’s account of what upkeep used to hold. What was cut is the measurement and the history of finished work; what stays is every open item, every constraint a future change has to respect, and the two numbers a refusal has to be read against (the locks column’s 0/24/35, and the lending column’s 6 of 39).malloc stays unwritable by design. Its Built line, its §5 and its row in the index say so; the §4 note itself stands, because an ADR is written once and the correction belongs to the record that made it.A length beside a buffer is checked at the call (ADR-147 D2), which closes the overrun 0.0.51 left open.
NK1159: a count that cannot be shown to fit its buffer. A &[T] and, right after it, a usize are one fact in C — the pointer says where and the count says how far — so a call passing a longer count is the buffer overrun the boundary exists to stop, and it is refused here rather than by the operating system.usize is not a type this language’s own values have (ADR-048 D1): a length here is an i64 and the machine-width type left the surface a program can write. A declaration that writes one beside a buffer is therefore naming C’s size_t, and this is what that says.len(), and a constant the buffer’s length is known to cover, which is an Array[T, N] (ADR-152 D1). A zero fits any buffer, because no length is smaller. A Vec with a constant is refused, and correctly: nothing here can show what its length will be.keeps::moves did not name usize or isize, so a type it did not name was one that moves and ADR-094 D1 lent the count: read(0, buf.as_mut_ptr(), &buf.len() as i64), which is not Rust. Invisible until now because no program could write a usize — the first declaration to name one is the first program to meet it.usize parameter had no way to take this language’s i64. buf.len() met NK1102 with a help naming as usize, a cast into a type Part I 2.2 does not offer. A size at the boundary now takes an i64 and the conversion is emitted — the arrangement str::repeat already had, arrived at from the declaration rather than from a name.usize beside a buffer that is not its length — an offset, say — has no way to say so, and the call is refused. That is D2’s narrowness rather than a defect in it: the record chose the pair as definitional. Written into ADR-147 §5 so the day a program meets it, it is met as a decision.CStr are the record’s steps 3 and 4 and are next, in that order.The C boundary lends a view, which makes most of C callable (ADR-147 D1, D5).
&T, &mut T, &[T] and &mut [T] in an extern "C" declaration, each the pointer C wants and each living for the call — which is what a view is everywhere else in this language (ADR-094). Nothing is stored and nothing escapes, so the shape that dangles cannot be written. Before this, every C function with a pointer in its signature was unwritable: getpid compiled and nothing with a buffer did.&[T] is a fat pointer, which is why a declaration writes the address. A declaration that lowered &[u8] to Rust’s own would be a signature the two languages disagree about, and what a reader would get for it is rustc’s improper_ctypes about a file nobody wrote (Part III C.1). So a foreign declaration has a type writer of its own.bytes becomes bytes.as_ptr(). A foreign declaration is a name in the very file the emitter is writing, which it already resolves by name (ADR-011 D2) — and one call serves every caller, because a Vec, an Array and text all answer it.&[u8] takes whatever lends a run of u8 — a Vec[u8], an Array[u8, N], a String or a &str — and one direction only: nothing fits out of a boundary type, because what a C function hands back is an address and the value this language would make of it is D3’s handle or D4’s copy.NK1158: both forms are the boundary’s and nowhere else’s. Away from it, a parameter this language may change is written mut name: T (ADR-094 D3) — the word goes in front of the name, because what it decides is also what the caller sees — and a run of elements is a Vec[T] or an Array[T, N], each of which carries its length.malloc(size: usize) -> Pointer[u8], which D5 makes permanently unwritable, so the page leads with strlen instead — and the specification’s lowering floor went up, 55 to 56.crates/nikaia/tests/foreign_pointers.rs, one of which compiles the lowering with rustc against libc’s own strlen and abs and runs it — no library the machine may not have, and the whole of D1 end to end.malloc stays unwritable, and Pointer[T] stays NK1135. Memory this language will index has to arrive with a length it knows; malloc hands back an address and no length, so there is no shape here for it. That is the decision rather than a gap, and a program that needs a buffer makes one in Nikaia and lends it.CStr are the record’s steps 2 to 4 and are next, in that order. Until D2 lands a call may pass a longer count than the buffer holds — inside unsafe, which is where C’s unsafety is written (ADR-124 D3), and still the overrun that check exists to stop. #240 carries all four.std’s ledger carries prose about every entry it publishes, which completes
ADR-139 §5 step 3.
nikaia.contracts ships with a package and is the one file a consumer’s compiler reads about a dependency; std is the surface every program meets first, and six of its hundred and nine entries had a doc line. The rest do now — written in that file and reviewed like code, which is the bargain it already makes about sync and crosses, because a compiler that does not read Rust cannot derive them.?? is for; that len() counts bytes and chars().count() is the other question; that a sort is stable, so two passes express a compound order; that drain hands its elements over owned rather than lent; that a Seen value is what a set(after) compares against; that trim hands back a view and costs no allocation.every_public_entry_of_the_shipped_std_ledger_has_prose fails on an entry added without a sentence, the way the two beside it fail on one added without a signature. A column that is mostly empty is a column a reader stops looking at, and six out of a hundred and nine was the shape that was heading for.doc is the one value in this format that can hold a line break, written \n and read back; rendering the shipped ledger and parsing it again holds the escape and its reverse together over a hundred real entries rather than one fixture.nikaia doc, which is that record’s §4 and wants a record of its own — so it is one piece of work with that one rather than a job waiting on nobody. #102 carries it.The array literal’s answer descends with the literal, which closes the defect 0.0.48 filed against itself.
let grid: Vec[Array[f64, 2]] = [[1.0, 2.0], [3.0, 4.0]] came back this is Vec[Vec[?]], and the let says Vec[Array[f64, 2]] — the literal takes the array type from its use, and the first reading of that read the type it was given whole, so a use one level down was not read. That is Part III C.4’s class and the one thing this compiler may not do.Vec[T] is walked through where an array is what it holds, and an array’s own elements are walked the same way, so Array[Array[i64, 2], 2] lowers both levels. A list of anything else reaches the rule and leaves it untouched: the walk is guarded on the wanted type actually holding an array.crates/nikaia/tests/fixed_size_array.rs, one per half of the fix.A fixed-size array is Array[T, N]
(ADR-152), which closes two records’ open
doors with one type.
Array[T, N], in the bracket generic the type language already has. Rust’s [T; N] is a new type form; this language writes every parameterised type in brackets already, so Array[f64, 3] adds no form and what a reader knows about Vec[T] transfers. It is N elements inline: in a struct it is part of the struct, as an argument it is passed as a value, and nothing is allocated — which is what makes it the answer to both ADR-127 §4’s C field and ADR-135 §4’s container that does not allocate, the one ADR-119’s bare-metal profile needs on its first day.Ty rather than a second kind of argument: Ty::Count(i64) beside the names and the tuples, so Array is an ordinary two-argument type and everything that already walked a type’s arguments walks this one. A second list beside them would have had to be threaded through every reader for one type’s sake.Array[f64, 3] against Array[f64, 4] is the ordinary argument-by-argument comparison, and no rule was written for it.moves, moves_away and copies each ask, one per caller — so an Array[i64, 3] is passed by value the way a tuple of three i64 is, and an Array[String, 3] is lent the way one of three String is. Without that the compiler wrote the & of ADR-094 D1 around an array of numbers, which compiles and is not what the record says.[0.0, 0.0, 0.0] is vec![…] or […] is decided by its use, a use is a type and the emitter has none (ADR-028) — but a statement’s byte is not enough here, because one statement may hold a list and an array both. So list_lit records the byte its [ stands at.Array[T, N] is wanted comes back as the array of what its elements agreed on rather than as the one that was asked for, so an element that is not what the array holds is the let’s, the argument’s or the field’s own refusal under the code that position always used. NK1157 is only the length, and it names both numbers because what a reader has to do about it is count.crates/nikaia/tests/fixed_size_array.rs, one of which compiles the lowering with rustc and runs it.let grid: Vec[Array[f64, 2]] = [[1.0, 2.0], [3.0, 4.0]] is refused although it is right: four positions write a use and each reads the type it was given whole, so a use one level down is not read. Filed as a defect in #248 with its reproduction — a correct program refused is Part III C.4’s class.extern "C" struct is still a parse error, so there is no repr(C) struct to put an Array[f64, 3] in. The field in an ordinary struct is built and tested.use std::collections::HashMap is refused, which completes
ADR-140: all five of its decisions are
built.
use brings no name in, for std as for a package. ADR-046 D2’s rule is the language’s and std was the one place it was not followed — and it turned out the compiler already did nothing with such a line: the module layer skipped every std import, the checker never looked at one, and the emitter wrote it back as a comment. HashMap works with no use at all. So what this removes is not behaviour; it is a line that reads like an import and is not one.NK1156, and the question it asks is does the name have a receiver. A module and a type read the same way in a ledger key — fs::read_to_string and HashMap::len are both X::y — so what tells them apart is the self: a type’s entries are called on a value and a module’s are not. And it must be a method of this name rather than of something inside it, or fs::Mapped::deref would make fs a type.use std::db::postgres and use std::backend::x86 name modules of a surface that does not exist, and refusing on one is Part III C.4’s correct program refused..nika files, three tests and three pages. Six tests in crates/nikaia/tests/std_imports.rs, one of which walks every .nika file in the tree.A doc comment is a language feature, and the ledger carries it (ADR-139).
/// before an item is that item’s documentation, and a pub one reaches the doc column of nikaia.contracts — derived like every other column, so a hand-edited one is overwritten by the package’s own build. nikaia.contracts ships with a package and is the one file a consumer’s compiler reads about a dependency; it had nowhere to put a sentence, and a comment in the source does not travel because the source of a published package is not what a consumer reads.item matches, the /// lines in front of it are consumed and gone — the same wall ADR-135 D3 met one record earlier. The skip records what it sees and the item reads it back.doc_here consumes nothing and asks at the one position where the question has an answer.item tries nine alternatives and eight of them fail after consuming tokens, so a flag saying the run still reaches here is false by the time the arm that matches asks.// line ends it — forbids the shape this repository is written in: a sentence for whoever reaches the item, then a note for whoever reads the source. A comment is trivia, so no token has been consumed. What ends a run is code.std is the first corpus. Its one Nikaia entry, text::digit_value, carries prose that is derived — and the test that regenerates std.contracts from its sources caught the drift the moment the line was missing, which is the whole argument for deriving it. Five hand-written entries a first program reaches carry one too; #196 holds the other ninety-eight.crates/nikaia/tests/doc_comments.rs.escape writes a line break as \n and unquote reads it back. A doc comment holds its line breaks and the file is read a line at a time. Nothing else ever written to that file has held one, so every ledger already on disk renders and reads back exactly as it did.throw, return, break and continue where an expression stands
(ADR-138), and two defects the record did
not see.
=> throw NotFound is a program, and so are ?? throw Missing, => return 0 and for x in xs { return x }. Each had to be written with braces that said nothing: there was no second statement they held together and no value they produced.Ty. What never has to mean here is this is not one of the answers that have to agree, and the only place that matters is a match’s arms — so an arm that jumps is skipped when the arms agree on a type, and the match is the other arm’s. The language below reads the jumping arm as the ! it is, so the two halves agree by construction rather than by a coercion this had to write.break outside a loop is still NK1132, a statement after one is still NK1133, and every program written against the braced forms means what it meant.examples/json/src/main.nika and Part I’s error chapter lose the braces, and the specification’s lowering floor went 53 to 54. Eight tests in crates/nikaia/tests/jump_expressions.rs.?? whose fallback jumps lowered into a closure. unwrap_or_else takes one, and ADR-084 D4 says a jump does not cross a function boundary — so let user = find(id) ?? throw NotFound(id), this record’s own first example, would have thrown from the closure while the program carried on and bound a value nobody produced. A different program, which is the worst kind of defect this compiler can emit. A jumping fallback is a match now; an ordinary one keeps the closure it always had.break stopped being a statement. break_stmt and continue_stmt are last in stmt, measured and argued in ADR-084 D8 — so once the two words were in the expression grammar, expr_stmt reached them first and every analysis that asks about a jump asks about the statement. NK1133, which is D3’s whole safety net, stopped firing. expr_stmt maps the three back in its action: nothing to parse, and the rest of the compiler gets the tree it had.The six match pattern shapes, which is the rest of
ADR-137 and the line the record opened
with: calc.nika matched step.0 because it could not match step.
.. for a struct’s rest. (0, 0), (0, y) | (y, 0), 200..299, (x, y) if x == y, Event::Click(Point { x, .. }), Point { .. }.Message::Write(text) is what it always was and Event::Click(Point { x, .. }) is the same shape one level deeper — the emitter, the binding walk and the refusal below all recurse, and none of them has a case for nested.NK1155: the alternatives of an | pattern bind different names. Every alternative binds the same set, because the arm’s body reads those names and does not know which alternative matched. Recursive, so an | inside a tuple’s part is the same refusal one level down...< is refused in a pattern (D4), naming the way to write an exclusive one: move the end. A pattern is one shape.calc.nika matches the pair: match step { (Op::Times, n) => … }, where it read step.0 and then step.1 in every arm. Eleven tests in crates/nikaia/tests/match_patterns.rs.NK1117 refuses the _000 of 1_000 and that NK1127’s call is D5’s second stage and is not implemented. Both were true before 0.0.33 and 0.0.36 and were reintroduced by the editorial pass, which rewrote the page from what it said rather than from what the compiler does. 1_000 is the number 1000, and the build-time call — and the loop — are built.A task nobody joined was left unwoken about two runs in five. Four instances of one mistake, and the test that had been reporting it was reporting it three times too rarely.
rt::io::generation) is what remembers that an answer arrived; pending is what says one is still coming. A reply lands in its channel, pending drops, and for a moment neither says anything — so a caller that reads pending first concludes nothing can move. exec::block_on then either panicked about a waker nobody arranged or spun out the whole cleanup-deadline and abandoned a task whose answer was already in its channel. That is ADR-055 D5 — a task nobody joins still runs — quietly false, and ADR-121 D3’s a hang is the failure this may not have with it.uring::Ring::park read unreaped == 0 && !elsewhere first. The generation check was already there, three lines below the early return that made it unreachable — so the fix is an ordering and not a new idea, and the comment that argued for it was right all along about a line it could not reach.io::park_for’s blocking half read pending() == 0 first, which is the same mistake on the other mechanism. Fixed the same way, because leaving it would be knowingly keeping one of the two.pending and then rang the bell. The other order’s argument was that a woken caller should see the finished count rather than the one before it; it costs nothing to be wrong that way round, because a pending that is briefly one too high only ever makes a caller wait instead of declaring the runtime stuck.exec::block_on rang the tasks’ alarms only after a park that waited. An I/O future stores no waker, so the completion count is the only thing that can say poll everyone again — and it has to be able to say it about a completion that landed before the round began.a_future_fed_from_a_worker_finishes_under_block_on runs its body twenty times. It catches a race, and a race caught once in two and a half runs is a test that reports no defect three times out of five — which is how this stood red through a long session while every individual re-run came back green. Twenty rounds of a coin that lands red two times in five come up green by luck once in twenty-five thousand runs.
E_CANCELLED; Part III 15.1, the ADR index and #88 said six.The third part is rewritten to the language standard, and the editorial pass
that began with STYLE.md is complete.
STYLE.md)std module pages and the availability table keep every row; the diagnostics table of C.3 keeps every code and its rule, with the NK21xx row that stood twice reduced to one.> **Implementation status:** <value>., twenty-eight of them, with plain facts and every diagnostic code and record link the old note carried.fs::lines and fs::bytes, which ADR-025 D3 withdrew; the target without an operating system cites ADR-119 rather than the extern record; two internal section references that pointed at sections that had moved point at the right ones.```nika block of the three parts is byte for byte as it was, checked by scripts/check-spec-blocks.py; the specification test is unchanged.The second of the three parts is rewritten to the language standard.
STYLE.md)?. rules of 3.5, the two hulls of SharedMut in 6.2, the narrow NK2101 of 8.3 — stand in the section’s prose. History, argument, test names and the second person are gone from the page.> **Implementation status:** <value>., with plain facts and every diagnostic code and record link the old note carried.unsafe { … } is read as a name (it is reserved with a construct, ADR-124), and a sum of fitting literals widens to i64 as 2.4 and ADR-063 say rather than being refused by the backend.```nika block is byte for byte as it was, checked by scripts/check-spec-blocks.py. Part III follows in the next package.The first of the three parts is rewritten to the language standard.
STYLE.md)comptime declaration and its stages, NK1127 against NK1152, the update_all refusal, the select and channel semantics — stand in the section’s prose.> **Implementation status:** <value>., with plain facts and every diagnostic code and record link the old note carried.yes executor work-stealing (it is a pool of futures over user-pool, ADR-055 §6), and expression capture is stated as decided against (ADR-143 D5) rather than as open.```nika block is byte for byte as it was, checked by scripts/check-spec-blocks.py; the specification test is unchanged.The specification gets a language standard of its own, ahead of the editorial pass that applies it.
docs/specification/STYLE.md, normative for the three parts: a section states the rule, then its consequence, then at most one short Design rationale that ends in the record; no history, no argument, no reader, one idea per sentence, lowercase common nouns, one word per thing (a [build] key is a build option, nikaia-runtime.toml holds runtime configuration). A status note has one form, > **Implementation status:** <value>., with six values: Implemented, Partially implemented, Not implemented, Reserved, Withdrawn, Unspecified. A rule is sober; a principle may be pointed, as a block quote after the rule it fixes.scripts/check-spec-blocks.py guards an editorial pass: every ```nika block of the three parts is a test, and the script refuses a pass that changed, added or removed one.docs/README.md point at the standard. The three parts themselves are rewritten to it in the next package.
a..b includes its end and a..<b does not, everywhere in the language — and
every range in the tree was rewritten in the same change, because the old
spelling keeps parsing.
..< is the exclusive range and .. is the inclusive one. for i in 0..<n stops at n - 1; for i in 0..n does not. Kotlin and Swift are the precedent and both read ..< as up to, not including. One meaning per spelling is the whole of the argument: the alternative the record turned down was .. exclusive in an expression and inclusive in a pattern, which is a rule a reader has to hold in their head and one the compiler cannot help with, because both forms parse in both places and mean different things...= is refused, naming 0..n as the form it was and 0..<n as the one that stops before its end. It was this language’s inclusive range; keeping it beside a .. that means the same thing would be the two-spellings defect introduced by the record that argues against them.{ is a body rather than a struct literal, and a spelling added to one and not the other is a form that works in half the language.examples/, benches/ and tests/samples/, every Nikaia source inside the compiler’s own tests, and Part I 3.3’s own example. A migration spread over two changes is a corpus that means something nobody wrote in between.crates/nikaia/tests/range_spelling.rs, and one of them walks every .nika file in the tree and fails on a range left on the old spelling — because a reader can check it by eye is exactly the claim a test should be holding instead.nikaia-std’s a_future_fed_from_a_worker_finishes_under_block_on is red about two runs in five, alone and under load alike, and has been through every change of this session. It is not a slow test: the drain at the bottom of exec::block_on is reached and parks, the started task’s readiness wait is never answered, and the 30-second cleanup deadline abandons it — nikaia: 1 background task(s) did not finish. The wait’s own deadline would have answered it at two seconds if the worker reported at all, so what is missing is a wake. ADR-055 D5 promises the task runs; it does not. Filed in #255 with the reproduction, because it is a defect in the executor’s wake path and this change is the parser’s — guessing at a repair beside unrelated work is how a second defect gets added to a first... for a struct’s rest. The range pattern is why the spelling went first and out of the record’s own order — a pattern range is inclusive, so it could not be written while .. meant the other thing. #218 carries the rest.1_000_000, 0xFF, 0b1010 and 0o17, and a number too wide for an i64
stops taking this compiler down.
1_000 was the number 1 beside a name _000 that nothing declares, and 0xFF was 0 beside xFF — a misparse, which is Part III C.1’s class and the one NK1117 was built to report rather than hand to rustc. An underscore stands between digits and nowhere else, and a float takes the separator (1_000.5) and no prefix. It has to: without it 1_000 stands beside .5, and .5 on a number is a tuple part.0xFF is 255, taking the first type that holds it exactly as 255 does. A width read off the digits would make 0x0FF wider than 0xFF — a type that depends on how many zeroes somebody typed.let n: i32 = 3_000_000_000 says 3000000000 does not fit, because that is the number, and reading somebody’s own spelling back at them says nothing.NK1117’s help lost its 1_000 clause, which is the point: it explained a misparse that is now a number, and a help text that explains a form the language has is worse than no help at all.extern rule, and for two reasons the grammar language gives rather than a preference. Its character classes are digit and any, so a hexadecimal digit and a binary digit have no spelling in it; and an action cannot fail. Measured, because it is tried at every primary position: 100 parses of the whole corpus, best of five, against the commit before it — the two are inside this machine’s noise (0.43s against 0.44s).crates/nikaia/tests/number_literal.rs.i64 crashed the compiler)let n = 99999999999999999999 panicked. The action read the digits with parse().unwrap(), so the program was wrong and this compiler fell over — Part III C.1 at its sharpest. It is a refusal now, naming i64 as the widest integer this language has.0b1210, 0o19. Without it the number ends at the bad digit and what follows is a second number nobody wrote, which is the same misparse one prefix along.The list literal, which language-review.md called the one piece of table
stakes this language was missing.
[1, 2, 3] is a program. The elements are expressions, a trailing comma is allowed, and the type is Vec[T] where T is what the elements agree on. vec![…] below, which is what a Vec already is there. Nothing in the grammar began with a [ in expression position, so the syntax was free and the only cost was the record’s two rulings.NK1154: two elements that do not agree, naming both. Where an element has no type yet — a bare 1 fits every numeric type, so it arrives as ? — it names the kind instead: this list holds a number and &str. That is what keeps [1, "two"] from reaching rustc as a question about a file nobody wrote (Part III C.1). Once per literal, because three elements that disagree with the first are one mistake.NK1153: an empty list whose element type nothing ever says (D2). [] carries none, so it takes one from the first use that needs one — let xs: Vec[i64] = [], or a later push — and a default would be a type nobody wrote (C.4). Asked once the whole body has been walked, because the use that answers it stands after the let, and asked only where nothing at all uses the name: a use this checker cannot read a type out of is left to the language below rather than answered wrongly.[ at the start of a line begins a literal (D3), and this was the piece with a cost. The generator skips the implicit whitespace before an alternative is tried, so by the time the index rule runs the line break in front of it is consumed and gone — and a PEG only looks ahead. The grammar now records where each run of trivia ended and whether it held a line break (Trivia, its one state), and a hand-written same_line reads it back; it consumes nothing. Measured, because it is on the hottest path there is — the implicit skip runs between the tokens of every syntactic rule: 200 parses of the whole corpus, six runs each against the commit before it, and the two are inside the noise of this machine (best 0.89s against 1.08s, median 1.06 against 1.10 — the new one ahead, which is what within-noise looks like). What it closed is C.1’s class exactly: println(f"{n}") on one line and [n].len() on the next lowered to println!("{}", n)[…].examples/ rewritten where a literal was what was meant: n-body.nika’s five bodies, escaping.nika’s three rows and tests/samples/jumps.nika’s four numbers were each a Vec() and a run of pushes. The ones built in a loop stayed as they were, because there a push is what the program means.crates/nikaia/tests/list_literal.rs, and the specification’s lowering floor went 49 to 53: Part I’s two list blocks and Part II 12.6’s and 12.7’s are programs now.mut nowhere and needed it)bench "Sorting" sorted a let that was not mut. It was a fragment nothing could read until the literal existed; now it lowers far enough to be refused, and the refusal is right — list.sort() changes what it is called on. NK1139, and the block says let mut list now. The block stays refused, because bench is still not a word this language has.let xs = [1, 2 moved. It was the clearest remaining case of the whitespace skip winning on progress — nothing began with a [, so at that position no real parser had reached it. Now list_lit reads the [, gets to the end of the line and requires its ]: the requirement leads and the , is in the note, which is the shape A1 and A2 already have.The build-time evaluator gets a loop, which is #178’s second step.
for over a range — 0..n and 0..=n — a while, break, continue, and an assignment, because a loop that cannot change anything is not a loop. fn total(n: i64) -> i64 { let mut t = 0 for i in 0..n { t += i } return t } is a function a comptime may call now, and comptime SIX = total(4) arrives as const SIX: i32 = 6;.Flow names the four ways a block ends (a value, fell through, broke, continued), and the frame is passed by reference now because a for has to see what its body assigned on the last turn.if had wrong. if i > limit { break } is the last statement of its block and hands back no value at all, so reading a last Stmt::Expr as the block’s value refused it. An if is a statement here wherever it stands, and what its branch does — fall through, return, break, continue — decides what the block does.while that does not end hangs the build. That is the cost the record accepted. The call depth stays bounded, which is the different failure it did not: a recursion without a base case would take this compiler’s own stack down with it.for binding is removed when the loop ends, so a body that reads it afterwards is unevaluable rather than answered with the last turn’s number.crates/nikaia/tests/build_time.rs, thirteen there now: both ranges, a while whose turns nothing counts, break and continue in one body, a return that leaves the function from inside a loop, and the binding that does not leak.NK1127’s note named the loop as the thing that is missing. It is not, any more. What is missing is a value: a comptime hands the language below what Rust’s const can hold, and today that is one integer or one bool — so a list a loop built has nowhere to arrive. The note and Part II 10.2’s status paragraph now say that, and #178 records what push waits on: ADR-135 and ADR-152, which are what give a list a type it can cross in.The build-time evaluator gets a call, which three records have been waiting on.
comptime initialiser may call a function of this program. comptime ANSWER = double(21) evaluates, and so does factorial(3) — a recursion with a base case. What a called body may be made of is arithmetic, comparisons, an if, lets, a return, and another call. crates/nikaia/src/build_time.rs is the interpreter; fold.rs stays in front of it, because it is what says which integer type a declaration pinned, which the interpreter does not ask and does not need to.sync (D1) and its touch set must be empty or exactly the build’s own parameters (D2) — and both are derived for every function the compiler sees. The interpreter reads what was decided rather than keeping a list of allowed functions.NK1152, deliberately not NK1127. One says this compiler cannot evaluate it — the staging talking — and the other says the rule says no. A reader does two different things about them: wait for a stage, or change the callee. A callee this unit does not declare is neither: there is no body here to run, so it is unevaluable, and saying you may not about a function whose body is elsewhere is a claim this cannot make.comptime name is worth is what was evaluated, not what folded. They were the same thing while the fold was the whole evaluator; with a call in it they are not — comptime ANSWER = double(21) was visible as a name with no value, so the constant below it was NK1127 although the one above it had just been computed. Found by writing the second constant, which is the shortest a fixture has taken to earn its keep this round.crates/nikaia/tests/build_time.rs, including the two refusals and the one that pins what is not in the stage: a loop is #178’s next step, and the note says so rather than leaving a reader to find out.0.0.7 (Draft), September 5. The CHANGELOG’s head says the version is the specification’s and every change package raises the patch number by one; nothing carried that across, so the number was raised in one file and read in another. Found by a reader, which is the part worth recording: it is docs/README.md §1’s class one level up — a stale Status note is a defect because a reader cannot tell a plan from a promise, and a stale version is the same mistake about the whole document.the_specifications_version_is_the_changelogs reads the newest ## [x.y.z] — date heading and asserts all three pages carry it. The CHANGELOG is the source because that is where the rule already lives; a second place to maintain by hand is how the first one went stale.Eight doors the records had left open on purpose, taken in one sitting — each
the way its entry recommended. docs/open-decisions.md is empty again.
Pointer[T] undeclared on purpose and that cost most of C — every function with a pointer in its signature. The thing to prevent is the dangling dereference, and the two shapes real C libraries are made of each get a form that makes one impossible by construction: a buffer is a view that lives for the call, with its length checked against it at the call site; a handle is an opaque type … released by … whose release is a cleanup. malloc stays unwritable by design — memory the language will index arrives with a length the language knows.select { … } keeps the first branch to finish, and a task handle has cancel(). Syntax and not a function, because an arm binds a name and then runs a block. It spends a keyword knowingly (ADR-084): overlap runs its branches and keeps every result, select runs them and keeps the first, and a language with one and not the other has half a pair. The semantics were already built (ADR-006 D3); cancel() costs nothing new, since the runtime already cancels a loser.std’s, and only bounded. send pauses when the channel is full, so a sync body cannot send on one and the ledger’s column says so without a rule about channels; recv hands back a T? and null is the ordinary end of a stream. No unbounded() — a capacity is a promise about memory, and a program that wants unbounded writes a large number and has said it.std::time::Duration, written 5.seconds(). A std type and a std extension; no suffix literal, because 5s would reopen for one type the form ADR-136 §4 declined for numbers, and a rule with an exception is one a reader has to remember rather than apply.Array[T, N]. Two records’ doors close with one type — ADR-127 §4’s C field and ADR-135 §4’s container that does not allocate. Rust’s [T; N] is a new type form; this language writes every parameterised type in brackets already. The genuinely new part is an integer argument in the type language.napi dependency in the emitted crate, which would make a second artifact and reopen which artifact is the program. Deciding now costs nothing and building now does not, which is the whole argument; writing it down means the shape will not have to be chosen under the pressure of the program that wants it.pub use at a time, and it carries fs, io, cli, html and task, so a program can read a file without saying so. The list is the containers, the printing functions, assert, panic and the numeric conversions; the rule behind it is nothing that does I/O but printing, nothing that pauses, which is also what makes it safe for a profile with no filesystem. HashMap is the first thing outside it.break carries no value, and NK1133’s help now names the two shapes that do. The refusal existed; the decision behind it did not, so ADR-138 §4 could reasonably have been read as undecided by whoever touched the loop next. A loop is a statement and hands back unit — one construct that is sometimes a value and sometimes not is the ambiguity if was spared by being an expression always. The help said bind it before the break, which is one of the two shapes and not the one a reader usually wants; it names the return one too now, second, because that is how a search loop is written.select, the channel and the duration each carry a Status note now — decided and not built. The convention stays on specification/README.md, because the next construct written ahead of a decision will want it.The question the last record made answerable, answered the same day.
match covers every case)NK1151. ADR-137 §4 left exhaustiveness open — a question about types this compiler does not yet answer. It was never open below: Rust refuses a non-exhaustive match, so the reader got the backend’s words on a Nikaia line, which is Part III C.1’s class. All this decided is whose message it is — and Part I 3.4’s own first sentence had been promising the rule the whole time.else (ADR-145), because the set of an i64, a char or a String is not something arms can write out. A bare name catches and covers, which is what the arm already meant.bool is the case that is neither, and it is written out: true and false are two arms and a complete match, which no enum map knows. Without it a program Rust accepts would have been refused here, which is C.4 exactly.crates/nikaia/tests/match_coverage.rs.One character back from a meaning it did not have.
match’s catch-all arm is else)else => … where _ => … stood, and _ in that position is NK1150 from the parser, with the replacement in the message. The last arm is everything else, and it was written with a character borrowed from the ignore pattern — which says something different: ADR-126 is careful that _ means ignore a value that arrived, neither bind it nor use it, and even argues against calling it a wildcard. Nothing arrives at a catch-all arm.else is already reserved and already reads as the branch taken when nothing before it matched (ADR-132); ADR-084 calls a keyword the most expensive thing a language adds, and this adds none. The lowering is Rust’s _, which is what the arm was before._ keeps every other position ADR-126 gives it — a tuple position, a parameter — and the nested one ADR-137 D1 will give it, for the same reason: those are values arriving. Three become two._ never parsed inside a pattern anyway, which is what made that free: a variant’s bindings are an ident_list and NAME rejects a bare _, so the whole-arm position was the only one a match had. No case analysis was needed to take it away.MatchPattern::Wildcard is MatchPattern::Otherwise, because wildcard was the wrong word twice over — nothing is matched loosely, and nothing is ignored.examples/, six blocks on the pages, ten in the tests, and the examples of the two records that write the arm — ADR-126 D1 and ADR-137 D1 — each with a line saying which record moved it.crates/nikaia/tests/match_else.rs.ADR-140’s fourth migration, and the record’s own plan for it was wrong in the one way that mattered.
std too)Vec(), String(), HashMap(), Stats(first). new was Rust’s convention reaching through a hand-written ledger, which is language-review.md §3.3’s second row; one convention stays and it is this language’s own. A written Type::new is NK1149, asked of the name and not of the position — so the constructor handed over as a value goes with it, which is the case the record names: par_fold(M, Summary, …) where 1brc.nika wrote Summary::new.Type::new is the name the lowering writes and the lowering is name for name (ADR-011 D2), so renaming the entries would have put a second spelling in the one place there must not be one. What moved is the resolution: the {name}::new fallback a .nika file’s own types have always had now reaches the library too, where it stopped at this unit. Vec() is one rule with Stats(first) rather than a special case.http::Server::new() names a package this build cannot see, and whether it should be http::Server() is that package’s ledger to say — NK1135’s convention, one refusal over.Self — right for a .nika type, which has no parameters, and wrong for Vec::new, whose entry declares -> Vec[?]: let xs = Vec() came out as a plain Vec and NK1106 refused it against every Vec[T] it was given to. The declared result is used where it names something; Self and the absence of a claim keep the old rule.sync walk looked the library up by one spelling. contracts::sync read the written name only, so a sync function that built a HashMap() was refused against a callee nothing described — NK2202 on nine programs at once. One loop over the two spellings..nika files and sixty-four in the tests, and the tests were the careful half: a test file writes Vec::new() in Rust as often as in a fixture. What is migrated is what is not followed by a ; and not inside a .contains( about the emitted Rust; measure.rs is entirely the second kind and is untouched..nika file name a Rust-side item at all: fixtures/measurements.nika says so in its own header and keeps writing Summary::new. The same shape appears in a slice of a real program, where a test extracts 1brc.nika’s grammar without its struct Summary, and that test now pins both halves — the slice writes Summary, the whole file writes Summary::new.new exists only for the default hasher (ADR-010 D5). Writing HashMap::new( straight out of the constructor arm took that back for every HashMap() in a trusted program — caught by the test that had been asserting the wrong half of it.crates/nikaia/tests/anonymous_constructors.rs.The last question of the previous round is answered, and the doors the records left open on purpose are gathered on the open page with a recommendation each.
project_status_and_roadmap.md: std::db first, the C library second, the query DSL third, the bare-metal target fourth, the HTTP server last. ADR-119 and ADR-125 schedule themselves accordingly, as if they always had.select and a cancel() on a spawned task; a channel in std, bounded only; the duration as std methods on integers; match exhaustive with _; break with a value kept refused; a fixed-size array as Array[T, N]; a native Node add-on not until asked, and then a generated C shim; the prelude as a small written list.The second of the questions is answered, and answering it found two more holes than it had asked about.
NK1148. The second declaration is refused, with the caret on the one that arrived and the note naming the first. fn, struct, enum, trait and grammar declare a name; a method does not — it belongs to its type, and two types may each have a len — nor does a rule, which belongs to its grammar and is reached as Json::value.trait and a grammar were not counted at all, so trait Foo beside struct Foo was accepted through every path. And everything else — two struct Foos, two fn fs, a struct and an enum of one name — was counted only when the build went through a manifest: nikaia --input skips the module layer, which is how the corpus, the specification’s blocks and a reader’s first program are all compiled. Each of them lowered and rustc answered E0428 about a file nobody wrote.modules.rs keeps its refusal for what only it can say — two files under one name, with both paths in the message — and skips a pair that is the same file, which it used to serve badly, naming one file twice in a sentence about two.NK1146 is always right now, because the case that made its help wrong cannot be declared: it says write Foo { n: … }, and that is the only Foo there can be.crates/nikaia/tests/one_name.rs.a_future_fed_from_a_worker_finishes_under_block_on (ADR-121 D3) passes alone a hundred times and has failed three times inside a loaded whole-workspace run, each on an unrelated change, with two clean runs after every failure. Its bound was raised once, ten seconds to sixty, and raising it again is the wrong fix repeated: the mechanism is contention, not slowness — io_workers defaults to one, the harness runs in parallel, and every in-process readiness wait queues behind the others on that one thread, so a wait ahead holds it for as long as its own timeout. #191 carries it as a suspicion with the three ways out, because no reproduction on demand exists.The LINQ question leaves open-decisions.md, and the answer’s first draft is
corrected on the way: the compiler checks no SQL, because it knows no dialect.
std::db is the protocol)meta::column(name, type), the third and last of the hybrid binding: a grammar declares the result columns and the compiler derives the row type from them as it derives the parameter type from the holes, so u.emial is nothing declares emial and a nullable column is T?.dsl sqlite(schema: app) { … } eod, named, no ;, resolved from a comptime asset — and the driver’s DDL grammar reads it, so a missing column is a build error at the query. No live database while building.std::db is the protocol and nothing more: the traits, the statement, the row values. http left std because nothing depended on it; db’s protocol stays because two drivers and every program that changes drivers depend on it.Seq combinators, the row type is the mapping. The sentence for marketing is D6.std::db section is rewritten; the roadmap’s “query DSL” says what it is.The first of the three questions is answered, and answering it pays back what ADR-140 D3 had cost.
NK2209. A rule’s action is arbitrary Nikaia, so it could call something that pauses — and nothing refused it: .await went into the synchronous parser the grammar! macro generates and the backend answered await is only allowed inside async functions, which is Part III C.1’s class for a shape the language allows. The demand is the one ADR-050 makes of an overlap branch and Part II 12.6 of a par_iter lambda, in the place a parser needs it: a parse that can be cut into pieces and run on several cores at once (ADR-009) is one whose steps do not wait on the world.init, step and merge are action code too (ADR-092), so the flag is set around the pattern’s lambdas and not around the block alone. Asked at the one place the free call and the method call already meet, and only where the ledger answered — a callee nothing describes is not refused (C.4). The message names the rule, because a grammar is a page of rules and a caret on a call inside one is not enough to find it.sync, and the ledger says so. Asserted and not inferred, because it is a rule of the language rather than a property of this grammar.async ADR-140 D3 had spread through every parsing program)contracts::sync reads this unit’s call graph, a grammar entry has no node in it, and holds.get(callee).unwrap_or(false) reads an absent node as pauses — the same trap ADR-109 D1’s comment names for a trait method, with the same answer: the entry is inserted as a leaf that holds. examples/inventory’s read is pub fn again and its ledger says sync = "inferred".keeps, touches and locks do not come back, and should not. D1 decided what an action may not do to time and nothing about what it keeps or touches, so read still has locks = "?" — a restriction added on doubt, which is ADR-010 D1’s polarity the right way round. Those three are the derivation #134 still names, and the record’s §4 says so.crates/nikaia/tests/grammar_actions.rs.docs/open-decisions.md §3 (§4 when it was written; the entry above it left the file the same day). LINQ to objects is filter, map and fold over a Vec and a Seq, and nothing is missing but a keyword. LINQ to SQL — an expression tree a provider translates at runtime — is the half ADR-088 §3 left as the one genuinely new capability, and Part II 10.5 already does the thing it was for better: SQL written as SQL, parsed at build time by the dialect’s grammar, every :hole a typed named parameter. What 10.5 lacks is the schema — an untyped row, a column the database finds — and that is one piece. Recommended: no expression capture; the schema read at build time through asset("schema.sql"), the statement’s result a derived struct typed from it, a missing column a compile error at the query; an ORM is not added because the row type is the mapping. The sentence for marketing is in the entry.docs/open-decisions.md was empty for one round. Three questions went back on
it, and all three were found by building rather than by reading.
.await is emitted inside the synchronous parser the grammar! macro writes and the backend answers await is only allowed inside async functions. Relayed onto the .nika line (ADR-056) and still the backend’s words about a construct this compiler let through. It blocks the sync half of #134: if an action may not pause, a grammar entry is sync by construction and there is nothing to derive. Recommended: it may not — a parser is computation over bytes that are already there, which is what makes @frame’s parallel parse sound, and nothing in the tree writes such an action.Foo(n: 1) is NK1146 and the function is uncallable through the only spelling ADR-133 D1 gives it, with a help that sends the reader to a line building the struct. Recommended: a name denotes one thing, refused at the second declaration — the sentence ADR-046 D5 already says one namespace over, and the only option under which that help is always right.ADR-140’s third migration, and the silence it made visible.
::)Json::value(input) where Json.value(input) stood. A grammar’s name is a name and a rule of it is a qualified name like every other; the dot is for a value’s members, and a namespace behind one was the single place this language asked a reader to tell two things apart by what the left side happens to be. The ledger key was already Grammar::rule, so the checker, the emitter and the contracts all read the shape the entry now has.NK1147 is the checker’s refusal and not the parser’s, because Json.value(x) and text.value(x) are the same five tokens and a grammar name is not a value. Raised only where the receiver is a grammar of this file and the name is one of its rules, so the message carries the whole rewrite and a method on a value that shares a spelling is untouched.examples/, five in the tests, Part II 10.2, 10.6 and 11. T::fields on 10.3 is the same decision on a page whose construct is itself unbuilt, so there the spelling was all there was to fix.uses_driver decided whether the emitted file imports ParseContext and Parallelism by looking for a method call, so after the migration a parallel entry lowered to a parse_…_pieces call with both names undeclared — rustc about a file nobody wrote (Part III C.1). No test of the spelling would have caught it; what caught it was every runnable example prints what it promises.NK1135, one position over. Part II 10.3’s describe[T: Struct] was refused as NK1117 — nothing declares T — because T.fields read T as a value; as a path there was nothing for that refusal to fire on and the block reached rustc, which answered cannot find trait Struct in this scope about a file nobody wrote (Part III C.1). A bound is checked against the traits either ledger records (ADR-106) and this unit’s own, with the message saying trait because that is what a bound names. A parameter with no bound is untouched, and a qualified name is left alone for NK1135’s own reason.parse_input entered its grammar through a dot, which made Json a name nothing declares, and is a program now.sync, keeps, touches and locks. The analyses answer nothing about a method whose receiver they cannot resolve, so the caller kept a promise nobody had derived — ADR-010 D1’s polarity backwards. As a call by name it reads the entry’s contract instead, and that contract carries none of those columns because nothing derives them: examples/inventory’s read lost sync = "inferred", keeps and touches, and its lowering became async. Every example still runs at both settings, so this costs information rather than correctness — and the derivation a pub rule’s action blocks would allow is #134, which is that section’s only live entry.ADR-140’s second migration, and a test that had been holding a claim it could not check.
throws and sync stand after the result type)fn f() throws -> String and fn f() -> String throws both parsed, and the specification wrote both — Part II fn add(…) sync, Part III pub fn read(path: Path) -> Bytes throws, Part I 7.1 fn fetch_config() throws -> String. The second is the language: it is the order ADR-102 D1 already fixes for a function type, where the trailing words are greedy, so a declaration and a type read the same way round. The pre-arrow form is a parse error whose message names the order.fn tick() sync { … } still parses, and that is what decides where the refusal’s cut goes: after the arrow, not after the word. A sync alone is not the old form; a sync followed by a -> is.examples/ — fifteen in errors_lowering.rs, one in returns.rs, one in function_types.rs, two on Part I 7.1. The corpus had never written the form the error-handling chapter taught.The one shape it takes away is one nothing can write. ADR-102 D1’s greedy rule gives the trailing words on fn make() -> fn(i64) -> i64 sync to the result type, and that record’s note pointed at the pre-arrow form as the way to mean the function’s own promise instead. A function type in a result is NK1142 until D5’s boxed lowering exists, so no program is waiting on the distinction; D5 owes it a spelling the day it lands, and ADR-102’s note says so now.
sync now stands before throws, which the record did not say and one slot decides. fn f() throws sync { … } used to parse because throws took the slot before the arrow and sync the one after it — legal only because there were two. The order in the one that is left is ADR-102 D1’s and D4’s own example. Refused with a sentence rather than with expected {, because the form was legal a version ago; nothing in the tree wrote it but one line of a test.a_trailing_sync_belongs_to_the_type_it_follows read the ledger, and sync is inferred from the body — an empty body pauses at nothing, so the ledger said sync whichever of the two the trailing word attached to, and the assertion held for the wrong reason. It reads the AST now: the declaration’s is_sync is false and the result type’s is true, which is the claim the record makes.The first of ADR-140’s five migrations, and the record that was waiting on it.
Name(field: value) goes)Stats(min: first, max: first) and Reading { name, temp } both built a struct, and Stats(first) called the anonymous constructor — so Foo(x: 1) went round a type’s invariants and Foo(1) went through them, told apart by a colon. ctor_lit is out of the grammar, in both the expression rule and the head one, so Name(field: value) is a call wherever it stands and Name { … } is the literal. Eight sites in examples/, thirty-four in the tests and six on the pages.NK1146: a call carrying options whose callee declares a type is the old spelling, and the message names the braces with the fields the line already has. Asked before the call resolves, or the answer would be about the anonymous constructor — the silent lowering was Stats::new() with the fields dropped, which rustc then refused about a file nobody wrote (Part III C.1).Widgit(size: 3) was NK1135; it is a call to a name nothing describes now, and silence is the correct answer to one — Widgit(3) beside it has always got the same, because this build cannot see whether a dependency declares it and refusing a correct program is what Part III C.4 forbids. The brace form still carries the claim.execute(target_age: 30) parses, and the leading ; is refused at a call too. D3 argued the parser could tell the form apart on the second token because nothing in expression position begins with a name followed by a colon; the named literal was exactly that shape, which is what blocked the half. With the literal gone the premise is true as written — since ADR-140 and not before it — and the asymmetry the build was in is closed: one spelling per shape, in a signature and at a call.; either. D3 says ADR-007 D5’s typed spread keeps its ;, because it stands after a subject in every driver that has one, which is true of the signature — pub fn execute(&self; ...args: Self::dsl) — and not of the call: a receiver stands outside the parentheses, so there is no zone for the separator to stand between. query.execute(target_age: 30), which is what Part II 10.5 already wrote.q.execute(target_age: 30) came out as q.execute(). The emitter fills options from the callee’s contract, and finding a method’s entry means resolving the receiver, which it cannot do (ADR-028) — so a method’s options reached the language below as nothing at all and rustc answered about the arity of a file nobody wrote. Reachable by the leading-; form just as well, so older than the spelling that makes it easy to hit.lent_args and nullable_args already use. The checker records the declaration’s option order (Checked::method_options, keyed by the statement’s byte and the method’s written name) and the emitter writes the same fill it writes for a call by name. A DSL driver is left alone, because what stands after its ; is one value rather than one argument each.script.exec(msg: message) is a program again, which is the one thing a floor is for.Every question the notes page held is answered, in one round and each the way that page had recommended. Seven records, one roadmap paragraph, and the specification’s own slips corrected on the pages that write them.
[1, 2, 3]. The grammar has no [ in expression position at all, so the syntax was free and only two things were the owner’s. [] takes its element type from the first use that says one — ADR-060’s rule for a number literal applied to a container — and is refused where none ever does, because a default element type is a wrong program that compiles. And a [ at the start of a line begins a literal rather than indexing the line above, which is the reading Rust and JavaScript both decline to take.1_000_000, 0xFF, 0b1010 and 0o17. Today each is a misparse and not a syntax error — 1_000_000 is the number 1 beside a name nothing declares — which is Part III C.1’s class and the reason NK1117 reports it instead of rustc. The radix is a spelling: 0xFF is 255 and takes the first type that holds it, because a width read off the digits would make 0x0FF a wider type than 0xFF — a type that depends on how many zeroes somebody typed.match patterns, and .. becomes inclusive. A tuple, an or-pattern, a range, a guard, a nested pattern and .. for a struct’s rest; a guard is if. The sharp one is the range: a pattern is a set of values and reads as one, so .. is inclusive there — and one meaning per spelling then means it is inclusive everywhere, with ..< the exclusive range in a for, a slice and a pattern alike. Kotlin and Swift are the precedent. ..= goes with it, which the question had not noticed it was asking for: this language already had ..= in an expression, so leaving it would be the two-spellings defect of ADR-140 introduced by the record that argues against it. Nothing in the tree writes a ..= range.=> throw NotFound, ?? throw Missing and an else that is one return are parse errors today, and the braces they need hold nothing together and produce no value. ADR-093 already gives the type, so what was left was the grammar and one ruling. Nothing about what they do changes, and NK1133 — a statement after a break in the same block is refused — is why this is safe rather than a quietly dropped value.nikaia.contracts ships and is the one file a consumer’s compiler reads about a dependency; it has nowhere to put a sentence, and a comment in the source does not travel, because the source of a published package is not what a consumer reads. A run of /// before an item is its documentation, the ledger gains a derived doc column on pub fn and type entries, and the compiler reads nothing out of the text — no directive, no link it resolves. /** … */ stays an ordinary comment, which keeps ADR-134 D3 true as written.Name(field: value) goes); the anonymous constructor, with new leaving std’s own types; :: everywhere, because the dot is for a value’s members; throws and sync after the result type, which is the order ADR-102 D1 already fixed for a function type; and use bringing nothing in for std as for a package. One record and five migrations, because they are one question and answering them apart would migrate the corpus five times for one property.execute(target_age: 30) and Stats(min: first) are the same five tokens, which is what ADR-133’s call half was blocked on — found by building it. With the named literal out of the language, name(field: value) is a call with options and nothing else, and the resolution fork the open question had proposed is not needed. The named literal’s migration goes first for that reason.project_status_and_roadmap.md: the HTTP server first because every demo stands on it, std::db, the C library (ADR-125), the query DSL, and the bare-metal target (ADR-119) after the server and the C library, which is where that record already schedules itself. It names no customer and no partner, because naming one is theirs to agree to.neg.is_some() in examples/1brc.nika is neg != null; Part III C.4’s postfix ? is gone, because a failure leaves on its own; List[…] is Vec[…]; the panic hook’s lambda loses its sync, because a lambda carries no promise and the demand is on the parameter’s type; restart_policy: "always" is an enum variant; and TimeoutError("Too slow!") is one too, because an error type is an enum. The last three were each a page contradicting the page beside it.5.seconds(), channel::bounded(100) and select { … } have no record at all. Removing them loses the shape somebody wrote down and the argument it was making; deciding them here would be three language features taken in a footnote, when each is a record’s worth of question — what a duration is as a type, whether a channel is std’s or the language’s, what select does to sync.docs/specification/README.md, because the two are not the same state: a Status note stands under a decided rule the compiler has not caught up with, an Unspecified mark under a construct nobody decided. The record said to put it on Part III and was wrong — the conventions that govern all three parts live in one place, and a second convention for the same reader belongs beside the first.List[…] is written in four places, not three — Part I 7.1, Part II 11, Part III 17.1 and Appendix B — and the Fortune one the table names is not on a specification page at all. And the 1brc.nika line was the only one that could break a program, so it is the only one checked by running the compiler: neg is the T? that "-"? produces inside a grammar action, neg.is_some() lowered to a method call on a receiver nothing typed — the silence ADR-105 §1 counted — and neg != null lowers to neg != None with the file lowering as it did.docs/README.md §1 makes a stale one a defect in its own right: Part I 2.2 for the number literal, 3.3 for ..<, 3.4 for the six patterns, 4.2 for the brace literal and the anonymous constructor, 4.5 for the list literal, 5.1 for the options-only call now that D1 frees it, 7.1 for the jumps and for throws after the type, 9.1 for use std::…, chapter 2 for the doc comment, and Part II 10.2 for ::...< would take each of those blocks out of the lowering floor for nothing, and the rule is what a reader needs from the page meanwhile. The migration is one change with the corpus in it, because the old spelling keeps parsing and changes meaning.docs/open-decisions.md is empty, for the first time since it was written, and says so. Ten entries left it: the nine that were open and the one the building of ADR-133 had added. the former backlog file gains ten entries in their place — one per unbuilt record and one per ADR-140 migration — and #218 is rewritten as waiting on a migration rather than on a ruling.Four words leave the reserved list, and the sentence each of them was paying for moves to the refusal every stray name already gets.
loop, const, macro and quote are names)NK1117 can say that sentence about an ordinary name. RESERVED_WORDS is 36 where it was 40; for loop in loops, let quote = '"' and a field called macro are names like any other.loop { … } is answered with write while true { … }, const X = … with write comptime X = …, and macro or quote with Nikaia has no macros; generating code from a type’s shape is comptime and a bound. Where the word is declared there is nothing to say and nothing is said.let loop = 3 looking for a keyword they never wanted.const and loop joined the emitter’s escape list, which is the record’s answered as it is for type: they were not there because a Nikaia name could not be one of them, and now it can. let mut loop = 1 lowers to let mut r#loop = 1, compiles and runs. macro moves back into the sweep reserved_below.rs keeps, because the escape can fire for it again; quote is a keyword in neither language and needs nothing, which makes it that test’s control rather than a fourth case.with did not leave (ADR-118): it is reserved for a construct, which is the one ground that holds.The ledger’s type language gets a word for what is produced step by step, and a measurement that one record made about itself turns out to be wrong.
Seq[T] and Par[T], the ledger’s words for a produced sequence)?, and no word for what keys(), chars() and io::lines() hand back: elements of a type, produced one at a time when asked for, with no length until the end. So those entries said -> ?, and because a method is found through its receiver’s type, everything called on such a value was unfound too. Seq[T] and Par[T] now parse with sync/throws after them for the step, write themselves back, bind through their item, answer as a receiver, and a for over one binds its item. A program cannot write either, and NK1135 says so by the rule that refuses Widgit.HashMap::keys, HashMap::values, String::chars, Vec::drain, HashMap::drain, io::lines, and six consumers under the receiver’s own word — Seq::collect, Seq::count, Seq::nth, Seq::join, Seq::map, Seq::filter.io::lines was the one that mattered. It said -> Lines, a named type whose iterates = "throws" column carried the failing step — which worked for the for and for nothing else: the binding had no type, so line.len() inside the loop was a method on ?, and six of examples/tally/src/main.nika’s .len() calls were among the ones the record counts. Seq[String] throws says both things in one place, and the fallible-step rule reads either.Seq rather than naming it: NK2701 reads a for over a sequence of String binds one name, because a program cannot write Seq[String] and a message that names a spelling its reader has no way to type is the shape Part III C.1 is about. It said Lines before, which the program could not write either.? that is a sequence is not what the corpus shows: 38 before these entries and 33 after, with the rest downstream of a receiver with no type for other reasons — tail.drain() in json.nika where tail is a field nothing types, map on the result of a catch in access-log.nika. MethodCalls::unanswered exists so that number can be checked again rather than remembered, and crates/nikaia/tests/sequences.rs holds it as a ceiling.par_iter has no entry, which is the record’s own it waits for a program read strictly: nothing in the tree calls it, and par_fold is a grammar driver and not this. The word is in the type language and binds, so the entry is one line the day something asks.NK2702. A produced sequence is walked once: the walk takes it by value, which is ADR-094 D2’s a value walked is a value kept, and there is nothing left afterwards. What counts as a walk is read off the signature and not off a list of names — every Seq entry writes its receiver (Seq[$T], …) and a container’s writes (&Vec[$T], …) — and a for over one is the other half. A Vec is untouched: a container has its elements already and is walked by view, as often as you like.NK2101’s analysis with one word changed, which is why it sits beside it and is answered at the same moment: the same question, an assignment in between revives the name, and only a plain name is asked about, because map.keys().collect() walks a temporary and a temporary has no second use to refuse. It has a tally line of its own for the reason the two NK21xx rules do — the way out is a collect() and not a throws.Seq[String] throws did not fit a Seq[String] throws. Found by the probe that gives a name another sequence, whose message read this is X, and what it is assigned to is X. Two sequences fit when their items do, with the two words read as a function type’s are, and Par fits Seq and not the other way round.
block_on test passed alone and went red under a loaded whole-workspace run. Its waiting task awaited a Slot filled from an ordinary thread, which is outside the runtime’s accounting: pending() can be zero while a task waits for one, and exec::block_on then panics about a waker nobody arranged — correctly, because nothing it can see will move. The task awaits a second worker operation now, which the runtime counts.io_workers is one by default and the harness runs the suite in parallel, so every in-process readiness wait queues behind the others on one thread and a wait ahead holds it for as long as its own timeout. A hang is unbounded, so a minute tells the two apart. Found by running the whole workspace twice rather than once.A character that had two readings, and the compiler’s answer was an accident of what the parser happened to accept.
_ is the ignore pattern, and it is no longer a name)_ stands where a name would be bound and nowhere else: a position of a destructured tuple, a parameter of a fn or a lambda, and the match arm it always had. Two _ in one list are fine — nothing is bound, so nothing collides — and a parameter written _ still carries its type, because the caller needs it._ parsed as an ordinary name, so let _ = f() compiled and lowered to Rust’s own let _ = — which discards the value where the source said bound. For a file handle or a lock guard those are different programs. x + _ parsed too, as a name NK1117 said nothing declared. Two of the record’s own sentences say both forms did not parse; #209 had found otherwise, and that is the defect this closes.NAME rejects a bare _, so the ignore pattern is producible in exactly the three positions and x + _ is a parse error. Two lookaheads are what _name costs: a bare _ is one followed by neither an identifier nor a digit, so _count, _0 and the _000 of 1_000 stay names — the last one matters, because NK1117’s a number is written in digits with no separators is a message about that name.let _ = expr is accepted by the grammar and refused by the checker (NK1144), which is the only arrangement that can carry the sentence: a refusal with a message has to come from somewhere that can write one, and a parse error at a character cannot. let (a, _) = pair() binds a and is untouched._, and the ledger writes _: Context in the signature column, where its keeps answer can only be not kept — a body that never names a value cannot store it — so a caller lends it. One of the seven tests is that claim through rustc itself: the emitted program is compiled without -A warnings, because the unused variable warning is what is being asserted absent and silencing warnings would make it pass forever.The last of 0.0.10’s three shapes, and the one every reader comes looking for.
else if)if a { … } else if b { … } else { … } was a parse error — expected {, found if — and the examples worked around it with sequential ifs, which reads as three decisions where there is one. The else rule has a second alternative and it makes that if the block’s one statement, so the chain reaches the rest of the compiler as the if inside an if it is: every rule of if holds at every link — the condition’s brace rule, the branches agreeing on one type where the value is taken, a return leaving the function — with nothing asked of any of them.else if where the else block holds one if and nothing else, so the generated file reads as the source does; what that takes away is the chain’s depth, since three links nested three deep is a line the reader has to unwind. An else whose block holds an if and other statements stays a block, because that is what the author wrote.elseif is a name — the grammar is scannerless, so a word it has no rule for is read as one — and NK1117 says nothing declares it. An elseif that quietly became the keyword would be a second spelling nobody decided on.examples/http’s status_line is one decision rather than three ifs in a row, which is the program the record was written about. A three-link chain is compiled by the same rustc that built the test and run: it prints ABCF, because the branches agree on one type and the chain ends with an else is a claim about a value and the only way to say it held is to run the program.nikaia fmt and the CLI has build, run and lower-std. Never unfold a chain into nested blocks or fold nested blocks into a chain is a rule for that tool to be born with rather than an unbuilt step of this record.Two of 0.0.10’s three shapes, one built whole and one stopped by a premise that turned out to be wrong about the grammar — and two defects found on the way to it, both older than the record that names their shape.
/* … */ is a comment, it nests, and it is not a doc comment)// to the end of the line and nothing else, which is enough beside a statement and not enough for commenting out a block while debugging, for a paragraph above a function, or for a note inside an argument list. BLOCK_COMMENT stands beside COMMENT in WS, which is the only place a comment can be said — the generator puts WS between the tokens of every syntactic rule, so a block comment stands wherever whitespace may. It nests, because the step inside it tries the nested comment before the plain character. /** and /*! are comments and the language still has no doc comment./* inside a string literal is text with no help needed, which is an argument about the grammar rather than a special case: STRING is a lexical rule, so no implicit whitespace runs between its characters and the comment rule is never asked. A // inside a block comment is comment, which is the case a scan that read line comments everywhere would get wrong — the close can be behind one.*/ missing — the condition is the grammar’s own, which keeps the one thing the scan cannot read (a /* inside a dsl … eod block, where it is text) from turning a good message into a wrong one.; is gone from a signature, and the call meets a question)fn execute(target_age: i64 = 0) parses: where one of Part I 5.1’s two zones is empty there is nothing for the ; to stand between. A mixed signature keeps its ; and keeps it required, and fn execute(; target_age: i64 = 0) is refused with a message naming the one spelling. Its own rule rather than config_param reused, because that one ends in a fail for a missing default — the right message after a ; and the wrong one before it, and a fail outranks the alternatives at its position.Name { leaves out the other struct literal: Kap 4.2’s Stats(min: first, max: first) is execute(target_age: 30) spelled identically. The parser tries the literal first. A name denotes one of the two constructs, so what tells them apart is resolution — which is open-decisions.md §9 with three options and a recommendation. The leading ; stays accepted at a call meanwhile: refusing it with nothing to replace it would leave such a function uncallable.execute(; target_age: 30) came out as execute(, 30) — the comma the emitter writes between arguments, written before the first one because such a call has nothing in front of it. rustc answered about a file nobody wrote, which is Part III C.1’s class, for the only spelling such a call had. Nothing in the corpus declares a function whose parameters are all options, which is why it stood.execute — C.1 again, and the very hole NK1135 (ADR-096) was built to close for a written annotation. It is NK1135 now, unqualified names only as that code’s convention has it, and its message names the call where the name is a function. That is the silence that let the collision above go unnoticed through two records.Row its first block declares, this harness hands each block over alone, and the block used to lower and come back from rustc as E0422. Blocks 28, 62 and 64 were already recorded that way for the same reason. What the number counts is programs handed to the backend, and one fewer wrong answer arrives from there.The ledger column that could only say yes, and the two refusals that had spent two records waiting for something to refuse.
crosses says no as well as yes)crosses = true was may and absent was undecided, so no described type could say may not and the refusals that fire on it had never fired. contracts::Crosses is the three values; the ledger writes both claims and stays silent for the third, so every file already on disk means what it meant; a third spelling is refused rather than guessed at, on iterates’ precedent — reading it as either value would put a claim in the file that nobody wrote.NK2501 and NK2502 have their first end-to-end tests, which is the first either code has ever had. Shared was the one type contracts::send answered may not about and ADR-037 D6 took that away; what answers now is a described type that says so. examples/foreign-runtime/’s three descriptions say it for the handle over an Rc<String> — the type the record exists for.crosses = true on a type with arguments is not read as permission, because the line may not have spoken about the T. crosses = false is read, arguments or not: a promise is withheld where it might not reach and a restriction is kept (ADR-010 D1), because nothing a value is wrapped in makes it crossable.NK2502 asks its question of a call nothing describes, which is ADR-038 D7’s own wording, so a described foreign call is not asked — and no column says whether a described foreign function puts what it is given on a thread. examples/foreign-runtime/crossing is that shape: its handle says crosses = false, it is handed to a function the description names, and it is still rustc’s Send bound that refuses it against the .nika line. A test asserts the silence so nobody rediscovers it; a column for it is a question and not work.nikaia describe writing the column, which is D2’s who rather than its what and waits on ADR-104’s step 2. Until then the line is hand-written, and the description says which part of the crate was read for it: inference = "described-from-signatures+fields", because crosses = false is the one claim there that no signature could give.0.0.8 decided the ring’s park and left it unbuilt. This is the package that builds it, and the only piece of the decision it does not reach is the one that turned out to be a language question rather than a runtime one.
exec::block_on either spun or panicked about a waker nobody arranged. The ring now carries an eventfd with a poll always armed and its own user data, and the bell writes to it as well as bumping the count the fallback park watches. Its completion is answered by draining the descriptor and arming the next poll, is never handed to a future, and is not counted among the ring’s outstanding jobs — so a park with only the bell armed still sleeps.io::read and io::read_to_string are the blocking read they always were, performed on an I/O worker and awaited: no second read shape, and no ring path for a stream that has no size to stat. What changed is the park. rt::io::wait has a future beside it (rt::io::waiting), which is the first thing the HTTP server’s socket layer needs.io::lines is the one entry left, and the reason is the language rather than the runtime. A step of it is an Iterator::next, and a suspension point inside one is while let Some(x) = s.next().await in the language below — a Stream trait Rust has not stabilised and a for over a stream this language has not decided. #182 is now that question and nothing else.uring’s soundness rule out of the bell entirely: IORING_OP_POLL_ADD hands the kernel a descriptor and no buffer, so there is nothing to keep alive and nothing to move. The eight bytes are taken afterwards, by an ordinary non-blocking read on a descriptor the poll has just called readable.io_uring_enter holding that lock and a worker that had to take it to ring could not ring at all — and per runtime rather than per process, which one hang taught: a test that builds a second Runtime gets a bell of its own, and publishing that one as the bell left the process’s own park deaf. The bell coalesces, so the park also checks the fallback’s own generation — read before the poll that found nothing to do — inside the ring’s lock, which is the whole of why a wake cannot be lost. And a park with a limit is bounded on this path too, by IORING_ENTER_EXT_ARG: after D1 the ring park waits for a worker operation where it used to answer nothing to wait for, so an unheard bell would be a hang where there had been a panic, which is D3 read strictly.Op::Readiness carries a duplicate of the descriptor rather than its number. While the only surface was io::wait the caller was blocked for the whole of the wait and its borrow was the guarantee; a future may be dropped while the operation is in flight, so the borrow is not there to be had. A dup shares the file description, which is what readiness is about, and costs one syscall.block_on on a thread it joins with a deadline, the other by a watchdog in the child process — because D3’s a hang is the failure this may not have applies to the test that reports it as much as to the runtime.else if: an else whose block is one if, with the braces left out. No keyword and no new rule — the chain inherits every rule of if — and the emitter writes Rust’s own else if.;: execute(target_age: 30) and fn execute(target_age: i64 = 0). The leading ; is refused, so there is one spelling; a mixed call keeps its ;. It costs the parser one alternative tried first, because nothing in expression position begins with name:. The two places the specification wrote the old form read the new one./* … */ is a comment; it nests as Rust’s does, an unclosed one is reported where it opened, and /** … */ is a comment and not a doc comment.open-decisions.md carries eight questions again, each with a recommendation: the list literal and its empty form, number literals, match patterns, a bare throw arm, doc comments and a ledger doc column, the two spellings of §3.3, what the specification writes that the language lacks, and the roadmap note for the marketing list.pub extern "C" struct is a struct with C’s layout that crosses the boundary by value — the Point and the Sample a C programmer expects to pass, not a handle around two doubles. Fields are numbers, bool, char, payload-free enums and other such structs, every one pub; anything else is NK1145 naming the handle as the shape. A list of them is an array in and the caller’s buffer out; the layout is in the ledger, so --locked catches a change; the same type serves a declared C function.symbol-prefix line in [build]; no declaration renames itself, so the header stays a projection of the ledger._async form hands back a ticket that <package>_cancel cancels at the task’s next pause point, with cleanup run and done called exactly once (E_CANCELLED when the cancellation came first). A stream is the callback the function already takes: fn(item) -> bool sync, whose false stops it and whose return is the backpressure. No construct is added.extern "wasm": a WebAssembly module’s exports are C-convention functions, and the target names WebAssembly. The same declarations make a .wasm with a generated .js and .d.ts; the host takes its buffers from <package>_alloc, a pausing entry point has only the callback form and the .js makes it a Promise driven from the host’s event loop.nikaia bind python over ctypes, nikaia bind js for the WebAssembly build — and never a second artifact. Java, C# and Go load the C library with their own means; a native Node add-on stays open.-7, the async form has its ticket, and its §4 keeps only the other direction.Closes what had been unreleased since 0.0.7 — the integrity round (the Seen
stamp, one update form, overlap keeping every failure, the exit status),
four reserved words freed and with given its construct, the bare-metal
target, the grammar’s vocabulary page, the ring’s park, crosses three-valued,
extern and unsafe reserved with constructs — and ends with its last
package: a library for other languages, and the ignore pattern.
pub extern "C" fn with a body is an entry point, artifact = "c-library" makes the package one, and the header is generated from the ledger. The shape at the boundary is the one every C library that has aged well uses — the caller owns the memory (buffers with size and written count, an allocator the caller may hand over before init), every call returns a status and puts its values in out-parameters, a struct is an opaque handle, an enum is numbered in declaration order. A panic is caught at the boundary and poisons the library; a pausing function is exported blocking and as _async; a handle carries a lock and a re-entrant call is a status. Nothing of it is built; #88 carries the seven steps._ is the ignore pattern: it stands in a tuple position, as a parameter of a fn or a lambda, and in a match arm, and nowhere else. let _ = expr is refused with NK1144 — the statement says the same thing — and _ is never a value. What it skips is not moved, so the view rule holds. It lowers to Rust’s _, and an ignored parameter produces no warning below. Only the match arm is built.rt::io::wait could not be awaited. An eventfd on the ring, always armed, that the bell writes to, makes every worker operation awaitable at once; a hang is the failure this may not have, so the test that a worker reply does not wake a ring park is inverted and kept.sync, run or kept — so a lambda that pauses fits a run parameter and the refusal #187 carried goes. The box this puts on the common case is measured before the record is closed. examples/fortunes.nika’s route handler is the program.crosses takes false, the claim a type may not cross a thread, written by nikaia describe from a foreign type’s fields; absent stays nothing recorded. The two crossing refusals, built and tested, can fire for the first time.catch Overtaken { continue }, and this language has one catch that takes everything; the retry is catch { continue }, exact where Overtaken is the only failure, and a typed handler is a record of its own the day a program needs to retry one failure and not another.open-decisions.md keeps one entry: what extern "C" costs the language.WS, => and # "…" mean lived in example comments. Part II 10.8 now lists every element a grammar may write, one line each, and is complete by rule. A rule’s action is the block after its pattern — the one -> is the result type, as a function’s — and the two names that were the engine’s spelling for what the grammar already says, tag("x") and digit1, are refused with "x" and digit+. The conventions stay, with precedent: uppercase is lexical as in ANTLR, WS steers skipping as pest’s WHITESPACE, => is the cut.std, which the availability table already decides per target. On the target user_parallelism is no, the emitted Rust is no_std with abort and no unwinding, the executor is the target’s with interrupts as wakers, and an interrupt handler is a fn() sync that touches no lock — so a handler that could pause or deadlock is a refusal, not a convention. A heap by default; allocation = "startup" refuses allocation after the executor starts, over a derived allocates column. A lock is a critical section the length of its block. The record says what the compiler promises about time (no hidden thread, allocation or lock; deterministic order for a given wake-up sequence) and what it does not (WCET, latency), and where the certification line is, without claiming it. After the HTTP server.with is a copy of a value with named fields changed)p with { x: p.x + 1 } is a new value of the same type with the named fields changed and the rest taken from p — the construct an immutable-by-default language wants most and had no spelling for short of a literal naming every field. The braces, the field list, the shorthand and the privacy rule are the struct literal’s; only the top level, so a field of a field is a nested with; the fields not named are moved, never copied unseen, and a value used again afterwards is refused with the copy to write. with on an enum value is left open and refused for now.loop, const, macro and quote are names)NK1117 already gives assert and unsafe — write while true, write comptime, Nikaia has no macros — so the words are names: for loop in loops, let quote = '"'. with stays, for the copy-with-changes construct the language lacks, which is the next record. The list goes from 37 to 33. What the language below reserves is escaped and does not decide this; how a word is taken after release is a question of its own, noted and not answered.from is a name, and a file a build reads is asset("…"))from leaves the reserved list. It bought one phrase — Json.value(from "config.json"), the read of a file at build time, which is not built — and cost the pair every API reaches for: the specification’s own fs::rename(from: Path, to: Path, …) could not be written in the language. The build-time read is now asset("…"), a call the compiler recognises in a comptime initialiser, with every rule of ADR-072 — a literal, the project root, the allowlist — unchanged under it, and the word Part III already used for what these files are. Rust does not decide this list: what it reserves is escaped.overlap keeps every failure)overlap fail, the first in written order still wins, and the others are no longer lost: every error carries a secondary list, the later failures join it in written order, and a cleanup error that fails while another is unwinding joins the same list — one mechanism for both, and a tree where the cases nest. The block keeps waiting for every branch, which is what makes the list a fact and not a race. A branch handles its own failure with catch and cannot see another’s; combining is catch on the block, with error.secondary in hand. catch and throws are untouched.T?)scores[name] on a map answers a T?, the same as get, because a key is data and may be absent — and a language that made null a type so that absence lives in the type had one spelling, on the container where absence is most ordinary, that went around it and aborted. m[k] = v still inserts; m[k] += 1 is written m[k] = (m[k] ?? 0) + 1, which answers what ADR-080 D2 had left open; a list’s xs[i] keeps its abort, because an index is the program’s own arithmetic and a wrong one is a bug. The read lowers to one index::get with an output type per container, as the write already does.??.?. reaches through a view)?. takes nothing. let name = user?.name followed by println(user) is an ordinary program: the reach is through a view of the receiver, and what comes out is a copy where the member copies and a view of the receiver otherwise, kept alive by the rules a field read already follows. The move Part I 3.5 called a rule was Option::map taking its receiver, which is the &-at-every-call shape again — a Stage-0 choice turned sentence — and ADR-052 D8’s translation of the move goes with the move. Nothing is copied that the program did not write.cleanup-deadline is a failure of the program)EX_SOFTWARE), and the message naming every such resource goes the panic path, standard error and the panic hook, never standard output. The specification had said “exits with a warning” and no status, and an unsaid status is 0, which is what systemd, cron and a pipeline under set -e read. No setting turns it back into success; cleanup-deadline = "0" does not drain and never expires.Seen[T])let stand = kasse.get(), a pause, kasse.set(stand + 100) — and its quieter twin, if stand > 100 { kasse.set(0) }, cannot be written any more: what a lock hands out is a Seen[T], the stamp sticks through arithmetic, through calls to anything whose touches names no lock, through declared fields and time, and never comes off, so a set given a stamped value or under a stamped condition is NK2205 wherever the read was, in the next line or the next request. An update block that assigns to v without reading it is the same mistake through the back door, NK2207. set(neu; after: seen) is the one door for a stamped value, defined as the update that compares and throws Overtaken. The emitter erases the type, so nothing exists at run time — the witness-at-run-time and the dataflow passes were weighed and are in the record’s §1. Perl’s lesson from ADR-108 holds here too: there is no .value, because a stamp that comes off with one word comes off early.catch Overtaken { continue } wants a catch that names a type, and this language has one catch which takes everything; open-decisions.md carries that question.update block says mut, may run more than once, and the compiler picks the lock)kasse.update fn(mut v) { v += 100 } is the one form of the write door: ADR-094 D3’s word with the lock as the caller, nothing returned (NK1141). What v is, is the compiler’s by type — a copy where the value fits a machine word, so the block runs on the copy, is compare-and-swapped in and runs again on a collision; the address in the lock otherwise, where it runs once. Nothing is moved out of the lock and no slot is ever empty, which retires the three-line push and the emptied lock. The retry ADR-039 §3 left open is permitted, because sync, the touches column and capture-by-view already make a second run invisible; a panic leaves a word untouched and an address poisoned as ADR-039 D1 says. update_all takes one mut per lock. access reads, and Part I 6.3’s sentence that it appends is withdrawn — the one place the specification contradicted itself. --sharing names a get on a type that does not copy cheaply.crates/nikaia/tests/lock_doors.rs asserts the old help text.sync)sync it may pause, without throws it cannot fail, and an implementation is compared against that — NK1129 becomes Disk::load pauses, and Source declares load as sync, and NK1140 is the same for throws. The emitter writes the return-position form in the trait, with Send where user_parallelism = yes and not at no, and async fn in the impl. The language below is Rust 1.75 or newer, the release that made async fn in a trait writable: every generated Cargo.toml says rust-version = "1.75" from one constant, and the compiler compares rustc --version to it before cargo runs. The channel stays rust-toolchain.toml’s; the compiler’s own floor is not claimed, because 1.75 cannot build it and nothing between has been measured.std function that takes a path takes its root right after it, with no default — a subject and not an option, because Part I 5.1 gives every option a default and a default here is the hole. The root is an fs::Root: Dir(store), under which the name is resolved and compared component by component and fs::Outside where it would leave, or Anywhere, the one way around the check, recorded per site and listed by nikaia --trust. No exception for a literal, since a relative name means whatever the working directory makes it mean. No provenance travels a path and no analysis follows one: the check is in the call, where the name and the directory are both in hand, and only std’s own comparison clears anything — the door Perl’s taint mode left open, where any regex capture counted as the check. fs::within and the compile-time refusal of an Untrusted path do not exist; ADR-058 D7 is http::File taking the same root, and ADR-010 D8’s lattice keeps its one consumer. open-decisions.md’s last entry leaves the file, and nothing in it is open.examples/ are the sites.&str is the assertion)String is the one text type; whether a value of it is borrowed, tethered or owned is the compiler’s per use, by Part I 6.6’s rule and as Bytes already behaves — so a literal stored where a String is declared is a view of static text and allocates nothing. &str stays, as the promise that a value is a borrowed view with no copy and no handle, held to at the line that would break it: the same door sync and @borrowed already are, for the loop where average is not good enough. A copy is .to_owned() or a refusal, never inserted; --tethers reports what was chosen; at a foreign boundary a Rust String is a copy the program writes. open-decisions.md’s entry on one text type leaves the file..to_string() in examples/ are the fixture.impls)[H: http::Handler] parses, because a bound names a type and every other position that names one takes a path; use is unchanged, no name comes in for a trait either. The ledger gains a trait table whose methods are ordinary fn entries, and an impl table written where the impl stands — so whether T implements A is the union over every ledger a program reads plus its own, and no ledger claims a complete list, which would make absence an answer. A call through a bound resolves to the implementing type’s own entry, since there is no dyn. open-decisions.md’s entry on the cross-package bound leaves the file.Seq[T] and Par[T])Seq[T] — elements produced step by step, with sync/throws after it saying what a step may do, as after a function type — and Par[T] for what par_iter() hands back, whose lambdas must be sync. A Seq is consumed by walking, so a second walk is refused in our words; a container keeps its name and is walked by view. Neither word is in a program’s grammar. What it closes: nine std entries that said -> ?, and the 35 unanswered method calls in the corpus that were all downstream of one. open-decisions.md’s entry on the ledger’s type language leaves the file.nikaia describe <crate>, which writes a draft entry for every function the program calls and the types their signatures name — from rustdoc-JSON where the toolchain offers it, from the crate’s sources where it does not — translated by Part III 15.2’s table, with touches and locks fail-closed and what neither reader can read written ?. The draft is contracts/<crate>.contracts, believed while the crate’s version and hash hold, and reviewed like code, which is what std’s own Rust half already gets. Every analysis then reads an entry at the boundary, never an absence, and C.1’s promise becomes keepable at the one place it was not.examples/foreign-runtime/ is the fixture either way.& at the call)&T in the lowering and given its & at every call, so page(entries) is the line, page(&entries) is refused, and the caller writes no reference anywhere the compiler writes one. Every & at a call in examples/ goes, and examples/report/src/main.nika’s comment explaining that count has to be read before page(entries, total) “consumes” them goes with it.contracts::keeps::lends is called by the emitter to write the declaration and by the checker to write the argument, because the two disagreeing is a &&T or a moved value in the language below — rustc about a file nobody wrote, which is the one thing Part III C.1 forbids.keeps column names; a value that copies, where a &i64 costs a dereference at every use and buys nothing; an argument that is already a view; and a method’s argument, for the reason touches gives for asking a weaker question — which entry acc.record(m) goes to is the type checker’s answer and the emitter has none (ADR-028), so a call the checker does not walk would hand a value into a &T.&str is a view in the declaration already, so what has to fit it is the argument with the reference the compiler writes — asking the other question refused count(dna) in k-nucleotide.nika, and asking this one everywhere refused record(Stats(2)). The refusal goes after the fit and never on a type nothing pinned: a &i64 handed to a &Request stays NK1102, and Ty::Unknown fits everything, which is the right answer for an absent claim (ADR-024 D1) and no ground to refuse punctuation on. But Unknown may not skip the writing — a guard placed one line too early took the & off inventory/main.nika’s render(entries, total) while leaving its parameter a view.NK1137 now covers both positions, the for head and the call, and its catalogue text says where it lands and where it stays quiet.sync is what says otherwise — lowers to impl Fn(A) -> Pin<Box<dyn Future<Output = R>>>, a call to one in the callee’s body carries an .await, and a lambda handed to one is |a| Box::pin(async move { … }). A parameter that says sync is the plain closure it was. A reader can tell what a signature costs by reading it, and nothing inferred stands behind it.async closure and the lowering had no shape for one. This is the shape.benches/handler). The ratio is large and the number is small, and which of the two matters is D1’s whole argument — a handler answering a request spends microseconds; a lambda run a million times over a list is the case sync is the door out of.std’s own entries are untouched is a condition on the check rather than a remark: HashMap::and_modify describes a Rust signature, which takes a plain closure whatever the ledger’s sync column says, and writing the future shape for it produced expected (), found Pin<Box<…>> against examples/access-log/src/main.nika. The shape is written only where the signature was declared in this language.sync = "from(f)" on ADR-029 D3’s reasoning that the lambda’s body is counted in the caller. That holds while the lambda is a plain closure and stops holding the moment the type may pause — then the lambda is a future the callee awaits, and a function that awaits is async whatever its caller handed over. So Sync::From is no longer an outcome of inference, and the run-kept question left contracts::sync with it; what that inference still answers is touches and throws, which are not this.extern and unsafe are words, and C is callable)examples/, in tests/ or in the three pages writes extern or unsafe as a name, and nothing is released — so the corpus is the whole cost, and the corpus costs nothing.extern "C" { … } is an item and unsafe { … } is an expression, in the same change. A word reserved so that a reader can be told something is one NK1117’s help can tell them about instead — that is what ADR-117 undid, and it is why these two arrive with their rules.sync and carries no throws, without writing either. Two things turned around against a trait method, and only one of them by this record: C has no suspension point at all, and a C function that sleeps blocks a thread — println’s question (ADR-067 D1) and not this one — so calling it pausing would make every C call an .await of a future nobody produced. touches and locks are fail-closed, as a described crate’s are: a C signature says less than a Rust one, not more.NK1143: the call is written inside unsafe { … } and nowhere else, which is the whole of what the word buys — the boundary visible at the call, in the body somebody reads, rather than in a file beside it. It is this compiler’s refusal and not the backend’s: Rust makes an extern block’s functions unsafe fn, so the program would be refused either way, with a message about a generated file.extern "C" { fn getpid() -> i32 } with unsafe { getpid() } prints a process id. What the page’s example still needs is Pointer[u8], a type nothing declares and one that record leaves open: a pointer that outlives what it points at is the one thing this language is built not to allow, and a type for one wants a lifetime story rather than a name. Eight tests in crates/nikaia/tests/foreign_declarations.rs. Two baselines moved and both the right way: unsafe is among the things an expression may start with now, and 15.1’s own block went from fragment to refused … [NK1135] — the page is a program this compiler reads and turns down in its own words rather than a shape the grammar could not take. One arm had to be added for that: the walk behind NK1135 did not visit an extern declaration’s types, so Pointer[u8] would have reached rustc intact.NK2504, and the message is the command: run nikaia describe hyper_shim. It was silent. Every analysis this compiler has reads a contract at the boundary — what may cross a thread, what the call may reach, whether it pauses, whether it can fail — and where there is none they all read the same thing: nothing. There is no silent third answer any more.type = "rust": a qualified name this compiler cannot account for — another package, a std path, a typo — keeps whatever message it already had, because refusing on a name nobody declared is Part III C.4’s correct program refused. Once per crate, because the reader’s next move is one command for the whole crate, with the caret on the first call. And hyper-shim in the manifest is hyper_shim in a program, which is the crate name Cargo makes of the key.examples/foreign-runtime/ is described rather than refused. The three projects carry a contracts/hyper_shim.contracts written by hand from the crate’s pub signatures — which is what D5 says to expect — with no touches and no locks on any entry: absent touches reads as touches everything and absent locks is that column’s third answer, and neither is something a Rust signature can tell anybody.LocalHandle holds an Rc<String> and is not Send, but that follows from its fields, and a reader of signatures does not have them; D3’s table says Send on a type means it may cross, and the absence of the word is not the claim that it may not. So the description carries no crosses, the crossing stays Undecided — C.5’s third answer, which is not permission — and the day the describer reads fields is the day NK2501 and NK2502 finally have something to say.nikaia describe itself and the hash rule: the description records the crate source’s SHA-256 and nothing compares it yet. Seven tests in crates/nikaia/tests/described.rs.sync = "from(f)" — the lambda decides — and one it keeps is answered from the type, so a kept fn() sync cannot pause and a kept fn() takes the claim away. A body that stores and runs gets the kept answer, which is the record’s §4 and the safe one. twice used to lower to an async fn every call awaited, and is an ordinary function now.Sync::From is an outcome of inference now, where before only a written std entry produced one — a value arriving in the greatest fixpoint where every other outcome is a promise being taken away. It is sound for ADR-029 D3’s reason and for one fact of this compiler: the walk takes in the body of a lambda passed as an argument and not only a trailing one, so every lambda a caller writes is counted in the caller, whichever parameter the column names. Reading such a call as does not pause instead would have been sync = "inferred" — a promise that fails open the moment a caller hands over a pausing lambda, which is ADR-010 D1’s polarity exactly.One thing the record had not said, and an ordering settled it. D3 says run-or-kept is keeps asked one level over, and keeps::infer runs after sync::infer. So it is asked in the small instead — the name is mentioned, and every mention is the callee of a call — which fails closed and agrees with the column on every shape either can see.
NK2206 for a lambda that pauses where the parameter’s type says sync, NK2606 for one that fails where the type declares none. Each is the shape of the refusal one level down — NK2202 and NK2605 — because a type that says sync is the same assertion a declaration makes, made about somebody else’s code. A lambda that does less fits a type that allows more, never the other way round, and a callee nothing describes leaves the question unasked (Part III C.4).NK2606 is the whole message. The function around the lambda is not the one that has to answer for a failure the type refuses, so NK2605 — this function can fail because … — no longer stands beside it pointing at the wrong function. Where the type does say throws the failure travels to the caller (ADR-029 D3) and NK2605 is right again.hand(fn(n) { … }) left n with no type at all, and the promises had nothing to be asked of. A free call resolves first now, which types its lambda’s parameters as well.And one test of the runtime stopped claiming something about the harness. a_worker_operation_does_not_wake_the_completion_park asserted the process’s pending-operation count was exactly one while the test harness runs its tests side by side; what it is about is that something is in flight when the park says nothing is, so that is what it asks.
fn(A, B) -> R, with sync and throws after the result, in the positions a declaration puts them. It was a parse error — the type grammar had a name, a view, type arguments and ? — so the eight std entries that take a lambda were written straight into the ledger where the grammar never saw them, and a second author could not write route, retry, sort_by or a panic hook.NK2206 and NK2606 — is step 2; today it is the ordinary NK1102.impl Fn(A) -> R, and -> Result<R, …> where the type says throws, which is the same Result a throws function’s own declaration has. fn twice(x: i64, f: fn(i64) -> i64) compiles and runs.NK1142 stands in front of the kept case. D1 lets a function type stand in a field, a result or a let, and those are the positions where it can only be kept — D5 lowers a kept one to a boxed closure over a boxed future, and that is not built. impl Fn(…) in a Rust field is not Rust, so it is refused here rather than handed down, where the answer would be about a file nobody wrote (Part III C.1).fn make() -> fn(i64) -> i64 sync the sync belongs to the result type. A function whose own promise is meant writes it before the arrow, which the declaration grammar already accepts.crates/nikaia/tests/function_types.rs.drive lowers every member dependencies-first and writes each package’s nikaia.contracts before its consumer is lowered (D5), so the package’s own answer goes back over the edit in the same run. Under --locked nothing is overwritten and D4’s comparison fails with the narrated diff, which is the message that case is owed.a_hand_edited_dependency_ledger_is_repaired_before_it_is_read takes sync off a package’s hello — the claim that would make its consumer .await an i64 — leaves the source hashes alone so D3’s belief still stands, and watches the build repair it and print 1. It is written to go red the day a ledger can be believed without being checked: a registry package’s (ADR-103) or a hand-written description of a foreign crate (ADR-104), either of which makes D6 writable and testable in the same change.set(neu; after: seen) is the one door for a stamped value)kasse.set(neu; after: stand) stores only if the lock still holds stand, and throws Overtaken otherwise — by definition kasse.update fn(mut v) { if v == stand { v = neu } else { throw Overtaken } }. It is what makes the optimistic form writable: read, compute outside the lock, store if nothing moved. D1–D4 had left a stamped value exactly one way through, the update block, which decides inside the lock and cannot compute outside it.set. The ledger carries Locked::set(after) and SharedMut::set(after), keyed the way the call is written; after: is not a tunable, it changes what the store is. Two things follow from it being an entry: the witness is checked, lent and stamped through the one path every argument goes through, because it is an argument of that entry; and throws = ["Overtaken"] makes the failure travel the way every other one does — NK2605 where the caller has not said throws, a ? in the lowering, a catch that takes the Result instead.NK2205 asks of set, and set(after) is not set — the ledger’s own name decides, because the name already says which operation this is. So the stored value may be stamped and the call may stand under a stamped condition, and the same two programs without the witness are refused exactly as they were.seen: &$T, the & is the compiler’s to write (ADR-094 D1), and one written by hand is NK1137.NK2208, found the same way. set_after is a real function below, so kasse.set_after(a, b) would have been a method that exists, takes two arguments and hands back a Result nothing in the contracts describes — the failure dropped, and rustc left to say something about a file nobody wrote (Part III C.1). It is refused by name, which is the cheap half of one door: no second entrance to the compare, and therefore none around the stamp.crates/nikaia/tests/witness_door.rs and four in nikaia-std’s own lock module, two of which run: an overtaken witness leaves the value alone, and of two threads holding one witness exactly one stores. Part II 12.2’s new example lowers and compiles — the specification’s baseline records it — and the door’s table gains its row.catch Overtaken { continue }, and this language has one catch which takes everything. open-decisions.md carries it with a recommendation.cleanup-deadline is exit 70, said on the panic path)0. It ends it with exit status 70 (EX_SOFTWARE), and the message goes the panic path — raised as a panic so the program’s panic hook runs, caught, and then the status — never on standard output, which belongs to the program’s own output.50ms and one readiness wait that will never finish, and reads back both the number and standard error. Either half alone would have passed while the decision was half built.panic = "abort" that profile keeps the message and loses the number — the process is gone with the abort’s own status before the exit code can be set. Named in the record rather than left to be discovered.Seen[T])kasse.get() is a Seen[i64], and so is what access and access_all hand back. It is a type in the checker and in the ledger and not one below: the emitter erases it, so a Seen[i64] is an i64, a field declared Seen[i64] is an i64 field, and a signature with Seen in it is one without. No counter, no marker, no check at run time, no bytes.touches names no lock — with its result stamped in turn, which makes kasse.set(bumped(stand)) the same answer as kasse.set(stand + 1) without an analysis following the value. A callee whose touches is absent reads as touches everything (ADR-033) and hands back a plain value: the wrong answer that way costs a refusal that is not raised, the other way refuses a program.NK2205 in both shapes — the value and the condition — and NK2207 for an update block that replaces its mut v without reading it, a set through the back door.NK2205 was. It asked whether the argument contained a get on the same container, which caught one line and nothing else. Two things it let through are refused now: the same pair spread over two lines, which it called a question about what happened between two statements; and a value read from another lock, because set’s own touches names one, so by D2 it takes a Seen only where its signature says so. The stamp does not record which lock, and that is the design rather than a limit.v = v + 1 mentions v and decides inside the lock exactly as v += 1 does, and a first build of NK2207 refused it. And a condition accumulates: a nested if under a stamped one is still under it.crates/nikaia/tests/seen.rs, and lock_doors.rs moved off the narrower rule — two shapes it called programs are refusals now.set(neu; after: seen) and Overtaken are the remainder. Until then a stamped value has one way through and it is update, which decides inside the lock.sync)sync it may pause, without throws it cannot fail. The declaration is lowered -> impl Future<Output = …> — the sugar async fn is not written, because async_fn_in_trait warns on a public trait whose future carries no Send bound and a warning about the generated file is Part III C.1’s class — and the impl writes async fn, which satisfies it.NK1129 became a comparison. It used to refuse every pausing implementation, because sync was asserted for every trait method (ADR-078 D4): a plain fn was the only thing the emitter could write in a trait. Now it is Disk::load pauses and Source declares load as sync, and NK1140 is its twin one column over. The other direction fits and says nothing: a body that does less than the declaration allows is correct. docs/language-review.md §1.3’s probe — a trait over a file read — compiles and runs.+ Send follows the executor, not the type. At user_parallelism = yes a task crosses threads and a spawn needs a Send future; at no nothing crosses and a Send demand would refuse the plain counts. It is a requirement of the executor this program is built for; whether a value may cross stays contracts::send’s answer and the same at both settings.Cargo.toml says rust-version = "1.75", from one constant in the emitter, and the build compares rustc --version against it before handing anything to Cargo — whose own package requires rustc 1.75 or newer names a package the author never wrote. A version this cannot read is not a refusal: refusing on a reading failure would refuse a correct toolchain.async fn when its declaration may pause, whether or not its own body does — written as an edge in sync::infer’s graph rather than a correction afterwards, so the fixpoint carries it to the main that calls it. The edge is drawn only where the trait is declared, because impl Error for ConfigError names a trait the compiler reads and an edge to a name the graph has no entry for reads as pauses — which would have made every error type’s methods async. And a dependency’s trait counts, which a test across a package boundary said.crates/nikaia/tests/traits.rs; two in package_graph.rs wrote a trait without sync and meant cannot pause, and the one holding the refusal says the word now.NK2203: a lock taken while a lock is held)locks column. Part II 12.3 called manual nesting an anti-pattern and “often a compile-time error”; D2 makes often into always, and with that no program exists in which a lock’s two representations behave differently — which is the whole reason user_parallelism may pick one.contracts::locks propagates touches a lock over the call graph so both are the same answer.println is one of these, which is the case ADR-067 D1 was written about: it never pauses, so sync says nothing about it, and it takes standard output’s own lock while yours is open. Two conditions and not one. print, println, eprint, eprintln and the eight lock doors carry locks = true in std.contracts now, and the corpus reads 18 hold, 7 undecided, 34 clear — every one of the 18 a print.spawn started inside the block, because its body runs later and elsewhere (D3’s own split, at the refusal this time — a test caught it); get and set, which hold nothing open (D10); and anything the column answers Undecided about, because doubt is not permission (ADR-010 D1) and not a refusal either. What silence costs is the runtime check, which is where every program already is.examples/ is refused, since nothing there opens a lock. One test had to move its println out of an access_all block — the first migration this rule has asked for — and Part II 12.2’s commented Compiler Error example now names the code it actually raises.crates/nikaia/tests/nested_locks.rs, half of them about the rule not firing.update block says mut, and the empty slot is gone)kasse.update fn(mut v) { v += 100 } is the one form: the block is handed the address in the lock, changes the value in place, and returns nothing. mut is ADR-094 D3’s word with its meaning unchanged, one position over — the caller whose value changes is the lock.Option is gone and with it the empty slot. update used to move the value out, hand it to the block by value, and move what came back in — so a block that panicked left the lock None, a state every other door had to report. D1 takes the cause away rather than the symptom, and the emptied() panic that named that state is deleted with it.NK1141 refuses a block that hands a value back (an update changes its value; there is nothing to return), asked of the shape and only where the shape says so outright — a last statement that can only be a value, or a return carrying one. A last statement that is a call is left alone, because whether that comes to a value is a question about its callee and answering it wrongly refuses a correct program. NK1138 is the other half at the same door.NK1138 of every lambda parameter, on the reasoning that one changed without mut is already broken Rust. It is not: par_fold(…, fn(acc, m) { acc.record(m) }) in examples/1brc.nika changes acc, has no mut, and compiles, because the emitter writes the word itself where it recognises a fold’s accumulator; and_modify fn(tally) { tally.bump() } in k-nucleotide.nika is the same shape one library over. Three examples went red before the rule was narrowed to a door.mut; tests/specification/COMPILES.txt carries it as a program rather than a fragment now.crates/nikaia/tests/update_doors.rs, three of them compiled and run, and four existing ones moved off the old shape — which §5 named as work.contracts::locks is a least fixpoint over the call graph sync uses — nobody has the property until something gives it to them, where sync starts from everyone having the claim and takes it away. A spawn’s body is excluded (it runs later and elsewhere) and a trailing lambda’s is counted (it runs during the call, ADR-029 D4). Recorded as locks, beside keeps.sync, where the cost of doubt is a caller writing .await. Taken literally here it gave the property to 16 of 59 functions in examples/ — almost all of them main, and not one of those programs opens a lock. A refusal reading that column would have refused correct programs, which is the one thing Part III C.4 forbids. So Undecided is its own answer, exactly as it is in contracts::send: not permission (ADR-010 D1), and not a refusal either. The corpus now reads 0 hold, 24 undecided, 35 clear.keeps landed in, and for the same reason: the number above is what a refusal has to be read against, and finding it out afterwards means finding it out from a program somebody wrote.get and set are among the most common method names a program writes.spawn the call was on, and D3 turns on exactly that. check::MethodCalls carries the task half separately now.crates/nikaia/tests/lock_column.rs, including the measurement itself: no example holds a lock, and that test goes red the day one does.docs/open-decisions.md has an entry again, and it came out of trying to build the one above it. Standard input does not suspend, and #182 had recorded the fix as wire it to Op::Readiness. That would not have worked.Ring::park answers off a count of ring jobs. A worker operation is not one, so a worker’s reply cannot wake the executor and no future may be fed from one: exec::block_on spins or panics with its own a future returned Pending without arranging for its waker to be called.a_worker_operation_does_not_wake_the_completion_park in crates/nikaia-std/src/rt/mod.rs holds the finding, and is written to go red the day it stops being true rather than to be rediscovered.rt::io::wait — ADR-038 D3’s readiness half, built for sockets — cannot be awaited either, only blocked on, for exactly this reason. That is what an HTTP server needs, so both #90 entries now point at the decision.rt::io::wait where it is.async fn is an infinitely sized future, and step 2 boxed a call that closes a cycle of pausing functions — resolving the callee’s name the way the emitter resolves anything, which is not at all for a method: stats.add(5) names add and only the type checker knows what it goes to (ADR-028). A cycle through a method reached rustc as recursion in an async fn requires boxing, about a file nobody wrote — Part III C.1.pausing_reach already draws an edge to every pausing method of a given name — the over-approximation its own note describes — so the graph had the method cycles in it all along and only the call site was not asking. The same widening answers it at the call: a box nobody needed costs one allocation, a box that was needed and is missing is a program that does not compile.crates/nikaia/tests/recursive_methods.rs, all compiled: a method that calls itself, a cycle of two, and the three that must not box — a pausing method that does not recur, a recursive sync one, and the free-function case step 2 already had. examples/json/src/main.nika’s four boxes are unchanged and still all free functions.NK2501 had always asked its question of what a task captures. A task’s body is an async block below, so a value bound inside it and still live at a suspension point further down is held inside the future — and the pool’s starter asks for Send of that whole future. The rule is send::held_across_a_pause: bound before a pause, and named after it.Crossing::MayNot has exactly one producer — a lock at a foreign destination (ADR-045 D3) — so at Destination::Ours, which is where a task goes, no type answers it. A refusal costs nothing before there are programs it would reject, and the same refusal added afterwards breaks them. What reaches it first is a described foreign type, which is ADR-104’s.Checked::held_across_a_pause) and seven tests in crates/nikaia/tests/held_across_a_pause.rs hold it on real programs. One of them holds the silence — that no type answers MayNot into our own code — because the day that stops being true is the day these programs start being refused, and that should be a test going red rather than a discovery.Undecided’s silence, so a name the walk is too generous about is refused only where a value of its type could not have crossed from anywhere. A call nothing describes counts as a pause, which is ADR-010 D1’s direction.mut)// x = 20 <-- This would cause a Compiler Error, and this compiler was not the one giving it. let xs = Vec::new() then xs.push(1), or let n = 0 then n += 1, lowered to a Rust binding with no mut on it and the answer came back about the generated file — Part III C.1. It had been there since mut existed; no example writes it, which is why nothing had noticed.NK1139 is the same rule as NK1138 one binding over, and two codes rather than one because the word goes in a different place and a reader is doing a different thing: a parameter’s mut also decides what the caller sees, where a let’s is only about this body. The help texts differ accordingly — write mut out, or take a copy if the caller’s value should stay as it was against write let mut xs.xs stops being the answer when the block closes and an outer mut xs is the answer again. Local became a struct for it. A set pushed and popped by hand at the checker’s twenty-eight scope sites would have been a correct program refused waiting to happen.examples/ was already clean and the specification’s blocks were too; the one program it found was in crates/nikaia/tests/concatenation.rs, which wrote let xs and then xs.push(1) — a fixture that had never compiled.crates/nikaia/tests/mut_bindings.rs (renamed from mut_parameters.rs, since it is now about both halves), one compiled and run.mut on a parameter: in-place change is written in the declaration)fn fill(mut out: Vec[i64]) lowers to &mut Vec<i64>, fill(xs) gains its &mut, and the call otherwise shows nothing — exactly as xs.push(1) shows nothing. A language that hides mutation through a receiver and shows it through an argument has two rules for one thing.signature = "(mut out: Vec[i64])" — because that is the one key a caller across a package boundary reads a parameter’s kind off. It is the parameter’s and not part of its type: it says who changes the value, which is what &mut T says below and what no type in this language’s grammar can. keeps::lends withholds its own claim on such a position, so the two never both write a reference.NK1138 closes a hole older than the record that named the word. A body that changed an owned parameter lowered to a Rust declaration with no mut on it, and rustc answered cannot borrow as mutable about a file nobody wrote. Raised only where the change is certain: an assignment into the parameter or into a place rooted at it, or a method every candidate entry marks mutates. A name a let has bound has stopped being the parameter — which is D3’s own way out said back, take a copy with let mut v = x — and a method no ledger describes is not one to refuse on (Part III C.4). Said once per parameter, with the caret on the declaration.calculate_physics(obj: Object), which does obj.x += obj.velocity — and says mut now. examples/ had none: every mutating call in it is on self or on a local, which is also why nothing there could have found the mutates hole below.crates/nikaia/tests/mut_parameters.rs, two of them compiled and run. Half are about the refusal not firing: a read, a shadowed name, a method nothing describes — because the other half of C.1 is C.4, and a correct program refused is the worse of the two mistakes.&mut self. Its type language spells a view &T and has no second spelling for a mutable one, so Vec::push and Vec::len wrote the same receiver type. Harmless while nothing read it; the moment ADR-094 D1 began writing a & off the keeps column, fn fill(out: Vec[i64]) { out.push(1) } lent out and rustc answered cannot borrow *out as mutable about a file nobody wrote.mutates is the claim, seven of std’s ninety-four entries — Vec::push, Vec::sort, Vec::sort_by_key, String::push, String::push_str, HashMap::entry, Args::nth. For a function this compiler reads the body of it is not inferred at all: &mut self is the declaration, which is D3’s sentence one position over. Locked::set and the other hull doors are &self in the language below and are untouched.examples/ is on self or on a local, never on a parameter. The test is in crates/nikaia/tests/lent_arguments.rs, beside its reading twin.contracts::sync never walked into a throw. throw ConfigError::NotFound(path) was a statement the walk stepped over, so the path inside it was neither read nor kept — and the moment callers stopped writing their own &, the argument was handed over by value. Walking it then exposed the second one.E::V(x) for a declared enum or struct is not a function and has no ledger entry, and the fail-closed answer for an unknown callee — it keeps what it is given — is the wrong one there. reached now says nothing about it.use http as h made h::id_of(made) look up h::id_of while the checker had recorded http::id_of, so the answer was never found. It is unaliased at both ends now — a hole nullable_args had shared since it was written, which is why no test had caught it.crates/nikaia/tests/lent_arguments.rs, five of them compiled and run, because the declaration and the call are written by two different passes off one column and the only proof they agree is the language below accepting both at once. Most of the file is about the claim being withheld — the kept parameter, the copy type, the argument already a view, the method call, the callee no signature describes — since a version of this rule that lent everywhere would pass the first test and break the language. One of them is the desynchronisation itself: an argument whose type the checker could not pin is still lent, because the declaration was.Json.value(input) enters a grammar, every pub rule is an entry, and dsl X from e is refused at the parse with a message naming the call — the shape ADR-022 gave fn:. One form in the language at every moment; no deprecation window with two spellings for one thing.entry_rule picked a grammar’s entry itself — the first pub rule, and a par_fold one ahead of an earlier one — so a grammar with two of them got one by source order, in silence. It is deleted rather than narrowed, and a_grammar_with_two_public_rules_is_entered_by_either is a grammar where the old choice would have taken the wrong one whichever was asked for.Json.value(input) was already a method call in this language’s grammar; what stopped it was NK1117 — nothing declares Json — so what it needed was a checker that counts a grammar name as declared and an emitter that recognises the receiver. A grammar name can only stand where a callee stands, so there is nothing to disambiguate it against.catch beside an entry is answered from a contract now. Every pub rule is an entry in the ledger with throws = ["?"], because a rule past a commit point can fail (ADR-023 D9) — so ADR-091 D4’s one line telling NK1134 that a dsl … from … can fail is deleted with the form it was for, which is what that record said would happen to it.uses_driver asked whether the picked rule is a par_fold, and with the rule named at the call it has to ask about the rule named — the lowering was right, the preamble was not, and rustc answered “use of undeclared type ParseContext” about the generated file. And a rule that is not pub needs its own sentence: Two.N(data) is an entry naming a private rule rather than a method call that missed, and there is no such rule, about one written three lines up, is a message a reader cannot act on.1brc, access-log, calc, config, inventory/stock, json, k-nucleotide, report), examples/README.md, Part II’s own blocks and its @frame example, and the three test files. grep -rn "dsl [A-Za-z_]* from" over examples/, tests/ and crates/ finds only the refusal’s own message.crates/nikaia/tests/grammar_lowering.rs, and the citations in #249 moved with the entry that closed: five sentences named an entry by its number, which is what that page’s own head says not to do, and each now names its subject.nikaia_<name>)http = "1.2" in nikaia.toml resolves on crates.io as the crate nikaia_http, through Cargo’s own dependency rename, so the prefix appears in one generated line and never in a .nika file or a use. "1.2" is Cargo’s semver, the resolved version is the lockfile column ADR-021 already has, a git table with a tag is the same arm without an index, and the path arm stays. A published crate holds the manifest, the sources and the ledger, plus a stub Cargo.toml that exists only for the resolver; the consumer reads and emits it as it does a path dependency. A crate under the prefix without sources and ledger is refused by name.open-decisions.md’s first entry, how a package is named by a version, which leaves the file — the registry, the version grammar and the lockfile were already in the tool every build runs, and the one thing to decide was the name.dsl … from … is a work order, not a question)pub rules gets one of them by source order), and the one change that closes it: the call form built, a throws on every generated entry, the old form removed and refused with the new one in the message, and everything that writes it migrated in the same commit — eight programs, examples/README.md, Part II 10.7’s own block, three test files. The record had carried a corrected count and a question about whether to deprecate first; the question leaves open-decisions.md, because the record had already answered it and only the size of the migration had been miscounted. #249’s entry is the work order, with the steps and what done means.
for lends, and a let over a place is a view of it)for e in entries { … } used to take entries away; it leaves it where it was now, so entries.len() on the next line is a program rather than rustc’s use of moved value about a file nobody wrote. let s = totals.stations[name] and let name = config.name are views, because the language below refuses to move a value out of a container or out of a borrowed field — a move there was never what the line meant.for i in 0..3 and for line in io::lines() are untouched..iter() and not &: the iterated name may already be a view — examples/inventory/src/stock.nika’s total(entries: &Vec[Entry]) — and &entries is then &&Vec<Entry>, which Rust does not iterate, while .iter() reads the same through any number of references and the emitter has no types to tell them apart (ADR-028). drain() had to be built: D4 names for x in xs.drain() as the written form of taking the elements away and std had no such entry, so a body that hands an element to a callee which keeps it had no way to say so — or_insert(counts) in access-log.nika, all.push(v) in json.nika, four loops in all. It is into_iter below, because D4’s words are removing a name from scope. And a let over a place that copies must not lend: let mi = self.bodies[i].mass over an f64 is a copy, and a & there is a borrow held across the loop that writes the same field — E0502, met in examples/n-body/src/main.nika.let half asks the checker and the for half does not. Which of the two a place is, is a question about the type, and the emitter has none — so Checked::lent_lets answers it with the same moves_away that NK2101 reads, and where it cannot type the place it says nothing. That silence is where every program already is, and it is the reason the new refusal is narrower than the record’s sentence: NK1137 refuses a & written in front of a for’s list, and not yet one in a let, because there the written & is still a program’s only way to say what the line means. #186 carries that limit as the next thing.crates/nikaia/tests/lending.rs, three of them compiled and run — the loop that leaves its list behind, a for over a parameter that is already a view, and a drain() that hands its elements to a push..drain(), two & came off (report.nika’s loop head and n-body.nika’s let), and two & stayed, at the two places the checker cannot type yet.NK1134, NK1135 and NK1136 were raised by the compiler and catalogued nowhere; Part III’s table carries all four now, and its Status note says NK1101–NK1137 rather than a range that stopped at NK1133. A code the compiler reports and the specification does not name is the same defect as a stale Status note (docs/README.md §1), one position over.fn(Request) -> Response is a type, spelled as a signature is, with sync and throws after the result. Without sync the code may pause and without throws it cannot fail — the reading a declaration has. A lambda that does less fits a type that allows more; a pausing lambda handed to fn() sync is refused here, where today rustc refuses it about a file nobody wrote. Whether the callee runs the parameter or keeps it is inferred — the same keeps question ADR-094 asks of every parameter — and only a kept handler’s promises come from the type; a run lambda’s still decide the callee’s own, as map’s do. @detached is never written.examples/http/ can declare route, and open-decisions.md’s entry narrows to the one thing left — a bound naming a trait in another package.keeps column, and what it says about the corpus)contracts::keeps infers, per function, which parameters the body keeps — stores into a struct, assigns into a place, hands back by value, gives to a task, or passes to a callee whose own parameter keeps it — and the ledger records it beside returns. Nothing reads the column yet, which is what that step is for: the answer could be diffed against the corpus before one call site changed.examples/, six keep a parameter; the other 33 read what they are given. Among the 33 is report.nika’s page(entries, total) — the function whose own file carries a comment explaining that count has to be read first because page “consumes” the entries. It does not, and the column says so. That comment is one of the two pieces of evidence ADR-094 was written on.std says which of its own keep: six entries of ninety-four — Vec::push, String::push, HashMap::entry, Entry::or_insert, and the two set doors, written by hand and reviewed like code the way that file’s Rust half already is. An absent keeps on a present entry means it keeps nothing, which is std.contracts’ own convention for sync said once more for a second column. A file that recorded nothing and left every caller to assume the worst would have been safe and would have left the language exactly where it is — the ratio is the feature.sync’s is a greatest one, and the difference is which direction is safe: sync is a promise and is taken away on doubt, keeps is a restriction and is added on doubt. So two functions that pass each other a parameter neither stores keep neither — which a greatest fixpoint would have got wrong here exactly as a least one would have got mutual recursion wrong there.spawn inside a spawn — each counts as keeping, because the two wrong answers are not symmetric: saying kept of a value that is only read costs a caller an owned argument, which is where every caller already is, and saying lent of a value the body stores emits a &T parameter whose body moves it — rustc’s error about a file nobody wrote (Part III C.1).touches already asks it: if no entry named ::push keeps its argument, this call keeps none whatever the receiver turns out to be. That is what lets rows.push(row) keep and rows.len() lend without a single type.crates/nikaia/tests/keeps.rs, and half of them are about the claim being withheld — a read stays a read, a view returned is not kept, a callee that only reads leaves its caller lending, mutual recursion keeps nothing. An analysis that answered keeps everywhere would be safe and useless, and nothing in the column itself says which of the two it is.fn add(&mut self, temp: i32) { self.min = temp } keeps temp, the value does outlive the call, and what that costs is nothing — deciding that it costs nothing is the emitter’s question.catch is named once)catch { … } did in silence was take a callee’s new failure. When a throws set grows, every handler over that callee is named in the build output, --locked fails until the ledger is regenerated and committed, and the commit is the acknowledgement — nothing is written at the handler, and a handler that matches on error is told the same as one that does not. The syntax of catch is unchanged. open-decisions.md’s entry on whether catch takes a pattern leaves the file: the guarantee did not need one.std.contracts writes throws = ["?"] on every entry. #233 carries it beside the NK2401 machinery it reuses.
open-decisions.md’s sixth entry, and the feature itself is in doubt rather than merely unscheduled. #189 had been carrying it as “the one piece that does not wait” and “the only entry that closes a security hole rather than an ergonomic one”. The second half stands; the reasons given for picking it up first do not.contracts::trust is a whole-program join, one answer for the program, and D7 needs a per-value one, a dataflow analysis over locals that happens to read the same lattice. And it said the rule was “testable against fs::map today” — std.contracts carries no provenance = "untrusted" entry at all, and the one untrusted source that needs no server (fs::map(path; trusted: false), ADR-010 D3) is not built: the ledger parses the word and no call site may write it.Untrusted[T] as a type rather than a pass: the same guarantee paid for in the type system, which needs no analysis and cannot silently stop working, at the price of every signature that handles a name.fs::within itself — one path resolution and a prefix comparison per name, which is the check a server performs anyway.NK1129 is gone)impl lib::Greeter for Fixed { fn greet(&self) -> String { return lib::hello() } } compiles and runs.project::dependencies_first, Kahn’s over the members — reversing members_of is not a topological order once a package is reached two ways). So http’s build has deeper in view and writes the answer down; app reads it. That is why the reverted settling pass was the wrong shape rather than an incomplete one — it derived on the consumer’s side, where the graph is smaller by decision (ADR-053 D3), and two builds of one library then disagreed about whether ok pauses. the_rules_that_come_with_a_path_dependency is the test that failed under it and passes under this.[sources] table: one unit by file name, and the SHA-256 of its bytes — the same digest the cache key already computes (ADR-021). A consumer believes a dependency’s ledger while the hashes match and derives that package again where they do not, so a stale ledger is a hash that does not match, never a belief. The file name and not a path, because a path would put the machine that built it into a file Part III 13.5 makes a pure function of the source tree.[sources] at all, and a ledger that will not parse. The last two are derivations rather than errors: it is a generated file a person may have edited, and nothing was recorded must not read as nothing changed — ADR-010 D1’s polarity, which is the whole reason believing is safe at all.lib::c::Id, a type nothing declares and no program can write. Ledger::published drops them, which is what makes the believed answer the same answer the derived one was rather than a larger one. Found by a_package_reached_through_two_parents_is_one_package failing, which is a real diamond and not a fixture built for this.--locked gained the check D4 describes, and it cost one line: every member’s ledger is compared byte for byte instead of rewritten, so changing a dependency’s source and running nikaia build --locked names that dependency’s ledger. Measured on a two-package probe.Ledger::infer_package takes the library now, which the older doc comment said it must not: a ledger inferred against a different library would be a different file for the same source. D1 and D5 answer that rather than ignore it — a dependency’s ledger is a build input, with one author, existing before its consumer is checked. The reasoning is kept beside the change rather than deleted.crates/nikaia/tests/dependency_ledger.rs for the believe-or-derive decision and the round trip, one in tests/project.rs that builds and runs the two-package handler and then checks the dependency’s own ledger is where it should be, and the six already in package_graph.rs. #228 is empty again; what was there is quoted in its place, because a defect that sat for rounds behind a question is worth a sentence about why.modules::Program::of called Ledger::infer once per file, so every fixpoint the ledger rests on stopped at the file boundary — and a callee in the file next door was in neither own nor std, which reach_of sets blocked for. Sync::No means “can pause or could not be vouched for” and every reader takes the first, so NK1129 refused impl Simple for Thing { fn go(&self) -> i64 { return plain(self.n) } } for a body whose only call is n + 1 two files away. It compiles and runs now.sync = "inferred" where the column had been absent for four functions, throws = ["LeereZeile"] where it had been ["?"], and touches = ["stdout write"] where it had been the empty list. The last one is the sharpest, because its polarity runs the other way: an empty touches means touches everything (ADR-033 D4), so an unresolved call did not read as an unknown — it read as an answer nobody could use, and two overlap branches over such a function ordered against each other for nothing.sharing stayed per unit, deliberately. It summarises the Shared values a body holds rather than folding a call graph, so there is no fixpoint to run across files; what it needed from its neighbours was the callee’s signature, and that is in the package-wide ledger the declaration loop now builds. The same reasoning the other way decided borrowing_structs: a Symbol is interned by the parse of one file and means nothing to another, so that set is computed per unit and the declared type names — which are strings — are the package’s.NK1129, calls_reader is still Sync::No, and a call that leaves the package is still unresolved — which is D1’s half and step 3. Without the twins this change would be indistinguishable from relaxing the rule, and #228 names that as the one thing that must not be done here: it would trade a false refusal for a silent miscompilation.crates/nikaia/tests/package_graph.rs, including the two-file program compiled and run, and one that the answer does not depend on the order the units arrive in — Part III 13.5 makes the ledger a pure function of the source tree and --locked compares it byte for byte, so the fixpoints walking a BTreeMap is a property to check rather than to argue.impl lib::Greeter for Fixed whose body calls lib::hello() — is still refused, because a path dependency’s ledger is still neither read nor written. Part III 13.5’s Status note and the ADR index row move too; a stale Status note is a defect in its own right (docs/README.md §1).nikaia.contracts is written by the package’s own build and read by every consumer, never derived on the consumer’s side — ADR-020 D5’s rule for std, for every package. The inference graph is the package, not the file. The header gains a [sources] table with a SHA-256 per unit: a consumer believes a dependency while the hashes match, derives it again where they do not, and compares contracts byte for byte only under --locked. A build with a hundred unchanged dependencies hashes and infers nothing; a stale ledger is a hash that does not match, never a belief.sync body is refused as pausing when the call leaves the unit — NK1129 on a body that plainly does not pause — in both of its shapes, and open-decisions.md’s question of whether a consumer derives a dependency’s contracts, which leaves that file. NK1129 itself is unchanged.&, and a review of the surface language)keeps column beside returns, and the compiler writes the reference at the call the way it writes the .await and the ?. A for lends, a let over a place is a view, and in-place change of a parameter is mut in the declaration, never at the call. &T in a declaration stays as the assertion, beside the inferred answer, as sync does. The one semantic cost — a kept value’s cleanup point moves into the callee with nothing at the call — is accepted the way close(self) already is and narrated by the ledger diff. Nothing of it is built; #186 carries the five steps.& at calls and loop heads in 913 non-comment lines of examples/, each repeating what the callee’s signature had said; for e in entries followed by entries.len() type-checks clean and is refused below in Rust’s words.docs/README.md §2): ADR-013 D7 carried two rows that put a & at a call (fs::map(&path), &totals.stations[a]) and a sentence saying a value read after a call is passed as a view. They were a Stage 0 gap written up as a rule; the record now reads as though they were never there.docs/language-review.md is the review this came out of: the surface language read as a newcomer would read it, every claim reproduced with a probe or quoted from the page — this, the catch-all catch, the two-line lost update, Part I 6.3 contradicting Part II 12.2 about access, from reserved against std’s own rename(from:, to:), no else if, no list literal, no digit separators, and the order in which turning the language inside out would pay. One text type is open-decisions.md’s fifth entry rather than part of ADR-094: a different axis and a larger change.http package, a program that reaches it, and what a package cannot yet take)examples/http/ is a Nikaia package and examples/hello-http/ is a program that depends on it by a path. It is the first place in the repository where the Nikaia arm of a path dependency is written down rather than described — the type = "rust" arm has had examples/foreign-runtime/ all along. The two resolve differently and the manifest says so beside the line: a Nikaia path is read relative to the manifest’s own directory, a type = "rust" path is handed to Cargo and read relative to the generated manifest under target/. That is why foreign-runtime/serve climbs four levels and this climbs one, and getting it wrong once is what made it worth a comment. tests/project.rs builds the real directory and checks the bytes, carriage returns included..nika files, so a directory escaped it entirely — and examples/foreign-runtime/ had been escaping it since it was added. It walks directories now: one is known if some runnable entry reaches into it, or if it is listed with the test that checks it, and that test file has to exist.Server::route(path, handler) cannot be declared: a parameter cannot be a function, because the type rule has a reference, a name, generic arguments and a ? and nothing else — fn apply(f: fn() -> String) is a parse error at the fn. Meanwhile eight std ledger entries spell a parameter f: fn(…) and the corpus passes lambdas to all of them, so the door exists and only std may walk through it. That asymmetry is now open-decisions.md’s fourth entry, together with what a signature would have to say about code it is handed: sync has from(f), touches deliberately has no such form and the ledger says why, and throws and sharing have never been asked. ADR-029 D4’s limit is what makes a server different from sort_by_key — from is for a lambda the callee runs before it returns, and a router stores one.trait, and it opens in one file and shuts at the package boundary. A handler stored in a struct and dispatched through a bound compiles and runs today, in one file. Across a package: [H: http::Handler] is a parse error, because a bound is one name; [H: Handler] after use http is NK1126, because use brings nothing in and a bound has no way to say where the trait is. That is the decision. Two more things behind it are defects, and the former backlog file stops being empty one round after it said the next defect found would go there:impl http::Handler for Fixed and never brings the trait into scope, so rustc answers the user with “items from traits can only be used if the trait is in scope; perhaps you want to import it” — a Part III C.1 message naming a rule the program has no way to satisfy, since there is no import to write. One file compiles, because there the trait is in the same module.sync body is refused as pausing when the call crosses a package. NK1129 fires on a handler whose body calls the package it implements against — while the same build’s own ledger records sync = "inferred" for both the callee and the method. Replacing the call with a struct literal makes it go away and the identical shape in one file compiles, so it is an ordering defect: the trait rule reads the column before the fixpoint has taken in another package’s entries, and a callee absent from that fixpoint is read as one that pauses. tests/traits.rs already says these rules need the finished column; it builds one unit and cannot see this.fortunes.nika’s entry said it waited on two runtime pieces and “neither is a language question”. It has one, and this is it. Measured while checking: given a manifest that depends on examples/http/, the file stops before any of the three — dsl postgres { … } has no hole and postgres is not a grammar this compiler has — so the driver question comes first and the handler question is what it meets after that.NK2204 names the door with the value in it. kasse = 42 looks like an ordinary assignment and is not: the lock has to be taken for the write. The repair is mechanical, so the help says to write kasse.set(42) rather than saying “use a door”, which would leave the reader to work out which of the four.Binary and to nothing else — so a literal, a name and a negation are rebuilt and anything else is an ellipsis. A help line that quoted the wrong thing would be worse than one that quotes nothing.NK2205 is syntactic on purpose, which is D10’s own line: the get written inside the set, on the same container by name. kasse.set(kasse.get() + 100) takes the lock twice and the value can change in between; reading one container while writing another takes each lock once and is an ordinary program; and the same pair spread over two lines is a question about what happened between two statements, which this rule does not ask.NK2201 and NK2203 need to know what is inside a door and what a chain of calls reaches, and NK2503 needs the reachability walk — two local rules beat five half-analysed ones, and the entry now says which three are left and why.set’s argument is the emitter’s, made pub(crate) rather than copied: a second walk over the same shape is a second thing to keep in step with the AST. Seven programs in crates/nikaia/tests/lock_doors.rs, including the corpus.let takes one name, or a flat tuple of them)let (user, rights, prefs) = overlap { … } and Part II 12.5 writes let (tx, rx) = channel::bounded(100) — a form the specification uses twice, for two different constructs, and defines nowhere. overlap { … } met this rather than made it: the construct is built and was reached by its tuple in the meantime (let r = overlap { … }, then r.0), which works and reads worse than what the page promises. That line is a program now.match has patterns already and neither site needs them; what the two write is a tuple whose arity is known, taken apart by position.contracts::order goes opaque rather than reorder a statement that binds several names, contracts::sharing records no handle so each part stays at ADR-037 D6’s atomic floor, and dsl skips because a deferred block is one value. The same polarity three times — an analysis that cannot answer says nothing rather than guessing, and each fails safe: no reordering, no atomic taken away, no parameters claimed.?, and refusing a destructure because it looks like the wrong width would refuse a correct program (Part III C.4). A written type is refused as NK1136 rather than ignored, and a nested tuple gets a sentence rather than a list of tokens — with a test that an unrelated unbalanced bracket does not get that sentence.crates/nikaia/tests/tuple_let.rs, including the shape compiled and run, and a part answering for the type it was taken from. And the specification sweep moved twice, both forwards: Part I 8.1.2’s own line stopped being a fragment and became a body the checker reads, and Part II 12.5’s channel::bounded block now lowers._. Checking whether to refuse a _ in the tuple form found that the single-name form has been accepting it all along: let _ = f() compiles and lowers to Rust’s let _, which discards rather than binds — a value bound to a name is dropped at the end of its scope, and one bound to Rust’s _ is dropped immediately. For a lock guard that is a different program. Nothing in the tree writes one, so it is filed as upkeep with the decision it needs, rather than narrowed here.?? entry, corrected rather than deleted: it said Part I 2.3’s nullable types are themselves a parse error, and let a: i64? = 3 lowers and runs. That claim was the reason given for the entry being upkeep, and the reason holds for a different one.comptime binding where an item stands)comptime MAX = 1000 at the top of a file was a parse error while the same line inside a body parsed, folded and ran. Part I 9.2 already lists Constants among the items pub applies to, so the rule for a constant another package may read was written and the syntax for one was not.Item::Comptime had been in the AST since that record, with its public field, and nothing produced or consumed it — a variant nothing reaches is a decision that looks built from the inside.=“. The fold, NK1127, the declared-type check and the spelling the emitter is handed are one thing in one place — and a place where NK1127 did not fire would be a place where comptime quietly means let, which is the promise the word exists for.comptime now appears in the list of what may stand where an item does, which is a parse error’s note getting truer.crates/nikaia/tests/comptime_item.rs, including the whole shape compiled and run — four kinds of initialiser, pub, and a function reading a constant declared below it.examples/foreign-runtime/ explained the boundary with a rule that is gone)docs/history/foreign-runtime.md itself all said Shared lowers to an Rc at user_parallelism = no (ADR-037 D3) and that Shared was unbuilt. D6 narrowed D3 — Shared is an atomic count at both settings — and ADR-064 built it. This is where a reader goes to learn what crossing means, so a stale explanation here is worth more than its size.Shared is built, the same source will compile at yes and fail at no“. There is no yes/no split left to be inconsistent about, and D6’s own argument is that paragraph’s reached from the other side. The correction is kept beside the prediction rather than written over it, because what that page is for is the finding and the finding includes how it was reasoned.Shared cannot be written into this shape at all now — ADR-061 D1 refuses one handed to code nothing describes, since D7’s per-value inference makes a Shared[Conn] an Rc for one value and an Arc for another in the same program and no foreign signature can name both. So shim’s LocalHandle stopped being a stand-in for something unbuilt and became the only way to build the shape: the question is answered with a value that exists rather than a stand-in for one.NK1117 since ADR-051 — “nothing declares q“. A type had nothing, so let x: Widgit = 3 lowered verbatim and came back as rustc’s “cannot find type Widgit in this scope”, about a file nobody wrote. Part III C.1’s rule held for one half of this language’s names and not the other, and ADR-064 had fixed one such name by building it while leaving the class untouched.OFFERED is Part I 2.2’s own set and already answers what as may name; the ledger’s types map is every type this program and std declare, read under both the qualified key and its last segment; and the declaration around the name contributes its own type parameters. What is left is a short table of names the emitter writes that no ledger declares.http::Response names a package’s type, and whether this build can see that package is ADR-046 D2’s question with a message of its own — saying “nothing declares it” about a name a dependency does declare would be Part III C.4.impl method’s, a let’s annotation, and every argument nested inside any of those.examples/fortunes.nika wrote List[Fortune] — a type name this language does not have; Part I 2.2’s prose calls the concept List (Vector) and every program writes Vec, which is what std’s ListExt is implemented for. The file never built for another reason and so never said so. And five specification blocks stopped lowering, every one of which was already error[E0425] below — “cannot find … in this scope”, from rustc, about the generated file. The refusal moved from the backend into this compiler, which is the whole of what C.1 asks for.impl User and -> Config declare their type in a neighbouring block, which the sweep compiles separately; NotFound(Path) and -> List[Row] name types the specification writes and std does not publish, which is fortunes.nika’s finding arrived at from the page’s side; and fn total(p: &Player) names a type the specification declares nowhere at all, as do Object and SqlParser.a_field_of_an_unknown_type_says_nothing wrote &Row, a type nothing declares, to mean a type the checker cannot see. What it is actually about is the fields, so its receiver is now a type std publishes whose fields its ledger does not record — the type resolves, the field cannot be looked up, and nothing is claimed about it.return)fn forever() -> i32 { while true { … } } was NK1104, so an accept loop, an event loop or a supervisor had to end with a return 0 nobody reaches — and a reader of that line cannot tell dead code from a mistake.while true { } is () in the language below and loop { } diverges and is !, so the refusal would only have moved from the checker to rustc, about a file nobody wrote. That record emits loop, so the checker now claims divergence in exactly the cases the emitter delivers it — the two halves agree by construction rather than by agreement.true, and no break bound to this loop. This entry’s own earlier version had argued the work was small because break and continue did not exist — “the analysis other languages need for this question is, here, one test on the condition”. That was true and is not, which is the kind of sentence that stays quoted long after its premise leaves.break is not bound to this loop at all. The two failure modes are not symmetric: a false “a jump leaves it” asks for the unreachable return this record removes, which is where every program already is, while a false “nothing leaves it” lets a body fall off its end and hands rustc a file nobody wrote.crates/nikaia/tests/never_ends.rs, including the emitted loop handed to rustc and run — which is what makes the claim safe rather than merely quiet. The two shapes that look like it and are not — a condition that is a name, and a while true a break leaves — still answer for their declared type.[#183](https://github.com/Nikaia-Language/Nikaia/issues/183) (#183) was named by two ADRs, two index rows, a notes page and an emitter comment. All seven now cite ADR-093, which does not move.impl http::Handler for Fixed does not put it there — so a trait a package publishes could be implemented and then not called. Part III C.1’s class twice over: the backend’s words about a file nobody wrote, and the remedy the message names — “perhaps you want to import it” — is one the program cannot write, because ADR-046 D2 gives this language no import at all.tests/traits.rs builds one unit and could never have seen it.:: in the trait’s name is exactly what says the declaration is in another crate, so a trait of this program’s own and a trait the compiler reads rather than a .nika file wrote (impl Error for ConfigError, ADR-023 D3) are both left alone without the test needing to know about either.tests/project.rs, printing what the handler returned — and failing with the message above when the loop is removed. Its impl body is a struct literal rather than a call into the package, and that is not tidiness: a call out of the unit is NK1129 today, so a fixture that made one would be testing two things and failing for the other.sync defect is a unit boundary, not a package one)NK1129. Ledger::infer runs once per unit, so sync::infer’s graph is that one file’s; a callee outside it is in neither own nor std and reach_of blocks on it. Sync::No’s own documentation says it means “can pause or could not be vouched for”, and every reader takes the first meaning.app → http → deeper, http’s own build settles and emits ok as a plain fn while app’s build cannot see deeper and still writes an .await. They agree today only because both are equally ignorant.NK1129. It is right about what it reads; what it reads conflates two facts, and making it quieter would trade a false refusal for a silent miscompilation — the worse half of docs/README.md’s list.dsl … from … is superseded and the replacement is unbuilt (ADR-082 D1: Json.value(input)), which is why the checker still knows only the old form — asked about, and worth writing down rather than leaving as a surprise in a comment. ADR-091 D4’s line now says it is on borrowed time: when the entry is a call it is answered from a contract like every other call, and the line is deleted rather than edited. The thing that has to travel with the migration is named: a generated entry rule must carry a throws, or NK1134 refuses all eight programs that write catch beside an entry.1brc, access-log, calc, config, inventory/stock, json, k-nucleotide, report — plus Part II 10.2 and three test files. The record now carries the count; whether removal is still right is a decision and is now open-decisions.md’s third entry, with both answers costed — remove-and-migrate-together is unseparable, deprecate-then-remove leaves a window with two spellings for one thing, which is the defect ADR-082 §1 names.par_fold one winning, so a grammar with two pub rules gets one of them silently.adr-077.md said §2.14 for the item form, adr/README.md said §2.13 for what a const waits on. The renumber is only what made anybody look. Both now name their subject, and #250’s upkeep entry counts eight.check-adr-refs.py reads a decision from a ### Dn heading; ADR-090, 091 and 092 wrote theirs as bold paragraphs, so every Dn in them was dangling and nothing said so until a sentence cited one. All fourteen are headings now, and the first citation of ADR-091 D4 is what surfaced it — the gate works, it just cannot fire until somebody points at the hole.init, step and merge are code)fold(N, zero, fn(acc, m) { nothing_declares_this }) lowered without a word and |acc, m| { nothing_declares_this } reached rustc, which answered about a file nobody wrote. The same walk is what refuses an undeclared name everywhere else in the language.examples/1brc.nika and examples/access-log/src/main.nika both write par_fold(RULE, Type::new, fn(acc, m) { acc.record(m) }, Type::merge) — a path as a value in two positions and a method on a ? in the third — and all of it goes through untouched.Expr::Closure’s arm already crosses a boundary and counts loops from zero, so a jump in a step meets NK1132 through the path every other lambda’s does; with the ordinary walk in place, the compiler reported the old function as dead code. Two walks over one construct are two chances to disagree.break … and this is not in a loop” and offered return as the way out — wrong advice inside a grammar action. It now says “the nearest loop is outside this lambda”, and the help is to hand a bool back. ADR-084 D6’s backstop in the lowering is untouched: a walk cannot answer whether it is complete, and the emitter’s refusal does not have to.head:N rest:fold(N, zero, fn(acc, m) { acc + m + head }) parses — so the walk runs inside the frame the rule’s pattern makes. Without it, head is NK1117. It runs outside expected, because what a fold’s init and step build is the parser backend’s running value on the way to the rule’s type, not the type itself.NK1117, NK1132, NK1128, and whatever is added next. None of it had to be written for the construct.crates/nikaia/tests/fold_lambdas.rs, and the corpus sweep among them asserts it found files, so a corpus gone quiet cannot pass by checking nothing.catch over an expression that cannot fail is refused)catch a match over a Result, so text.len() as i64 catch { 1000 } produced E0308 about the generated file, naming a match and an Ok arm the author never wrote — Part III C.1’s class. Found while building a fixture for ADR-090, which is why the reproduction is that record’s own opening line: the catch there was written to ignore its error and turned out to have none to ignore.NK2605 refuses a call that can fail where nothing handles it, and its rule is that the ledger is the fact — reported only where a contract says so, and silent about a callee nobody has written down. NK1134 is that standard read backwards.access-log, calc, config, json, k-nucleotide, report — were refused the first time this ran over examples/, all at once: each writes dsl … from … catch { … }, which can fail by ADR-023 D9 and carries no contract to say so, because a grammar run is not a call. A dsl template block and an asm block answer no answer for the same reason from the other side — what they become is the emitter’s business, not this walk’s.match would take the program the author wrote and quietly delete a block from it — including a return inside the handler, which ADR-034 makes the function’s return. A handler that can never run is a belief about the program, and a belief that is wrong is worth a sentence.(text.parse() catch { 1 }) catch { 2 } really does have nothing left for the second handler, and is not refused: saying so needs to know what a handler’s own failure does, which is ADR-034’s question. The outer guard is told no answer instead — the direction that cannot refuse a program that is right.crates/nikaia/tests/infallible_catch.rs, two of which fail if the dsl line is removed: the reproduction, four shapes refused, three left alone, the nested catch, a grammar run, and a sweep of every .nika in examples/ and tests/samples/.catch that ignores the error binds _error)error whether or not the handler reads one, so catch { 1000 } — the shape Part I 7.1 teaches first — reached the author as warning: unused variable: error, about a binding that exists nowhere in their program. Part III C.1’s class one severity down, and the second half of what ADR-074’s survey turned up.contracts::order::names_in_block collects every name a block mentions and runs over these very blocks for the ordering analysis; block_mentions is a public question over it. No second walk, and no second notion of “is this name read” — which matters, because a second notion is a second thing that can disagree.f"{error}" is a mention; it counts errors as errors and not as error, which was checked. The two ways of being wrong are not symmetric: a false “mentioned” writes error and lands exactly where yesterday already was, while a false “not mentioned” writes _error under a handler that reads it and does not compile. Only the second is a new failure, and it is the one an over-approximate walk cannot reach — so a walk that has to lean does work a precise analysis would have to be right for.emit::CAUGHT is what the emitter writes and what the walk is asked about; two spellings would be a binding that goes unread while the walk looks for the other, which is this defect arrived at from the other side.allow, for ADR-074 D3’s reason: the warning is rustc reading the emitted code correctly, so the emitted code is the thing to change. An allow would also be silent the day the warning is about something real.crates/nikaia/tests/catch_handler.rs, and two of them fail if the arm is reverted: a table of four handler shapes against the arm each must produce, a mention nested inside an if, the n-body line compiled by rustc with its stderr read for the word unused, and a handler that does read the error compiled and run — so the _error direction is checked by a program that would not build if it were taken wrongly.text.len() as i64 catch { 1000 }, cannot fail: the catch lowers to a match over something that is not a Result and gets E0308 about the generated file. Same class, different defect, and it is filed rather than fixed — #230 carries it as a catch on an expression that cannot fail does not compile, with the polarity that decides it written down: refuse on known not to throw, never on not known to throw, because an unresolved call says nothing and refusing it would be Part III C.4.<T> (which it has done since ADR-074), one said it about a trait whose method pauses (NK1129 since ADR-080), one said the qualified constructor path is a parse error (http::Response(status: 400) parses and lowers), and one pointed at a §3.7 that does not exist.check-adr-refs.py covers ADR numbers and has nothing to say here, and the guard for this shape was measured and not built because it produces a fifth false alarm.comptime initialiser, ADR-079 §3 wants a loop and push to build a table, and ADR-088 D1 wants a loop over a type’s fields. Each named it in a subordinate clause; none of them is the list people read to find something to do. #174 now is.comptime’s evaluator is fold.rs, 124 lines, and it knows an integer literal, a name that already folded, a negation, and + - * / %. No call, no loop, no text, no aggregate. What bounds the work is already decided — ADR-075 D1 and D2 — so this is an interpreter for a restricted language and what it must refuse is written down rather than invented on the way.winnow-grammar. #178 is the other half and the reason is not effort: running a grammar at build time by interpreting it would be a second implementation of the same semantics, and Part II 10.2 promises that one grammar means the same thing at both stages. With two implementations that stops being a property and becomes a hope — and the disagreements would land in implicit whitespace, repetition bounds, the commit point, frames, interning and spans, presenting as “this file parsed while the program was built and fails while it runs”, for the same file and the same grammar.winnow-grammar is a code generator: its model crate parses, validates and analyses the grammar language and hands the result to a macro that writes a parser — “intended to be used by procedural macros that generate parsers”. It contains no interpreter.??’s fallback is one value, or an expression in brackets)?? sits above the whole binary chain, so its fallback reached rightwards across every operator there is: a ?? 0 > 3 was a ?? (0 > 3) while looking like (a ?? 0) > 3.a: bool? = false, x: bool = true, y: bool = false, the line a ?? x == y is false and (a ?? x) == y is true. Both are well typed, so nothing downstream would have caught it: a silent wrong value, which is the worst thing in docs/README.md’s list.unary_expr. A literal, a name, a call, a field, a -1 and a bracketed expression reach it; no binary operator does. The chain survives, because the rule recurses on its own tail (ADR-066 D4).?? above comparison so the line means what it looks like — which is what Swift does, while C# and Kotlin read it the way this language did. The languages disagree, so whichever precedence is chosen somebody reads the line wrong and gets no message at all; making the author say which they meant is the only answer right for both readers. And the polarity settles the rest: a narrowing can be relaxed into a precedence later, a precedence cannot be un-chosen. It costs nothing today — every ?? in examples/ has a fallback that is a literal, a name or a call.0, so the backend’s message is about an enclosing rule’s brace and the caret lands on a token nobody typed wrong. A note is added when the rendered message shows a ?? to the left of the caret and an operator at it, naming both readings. The offending token is deliberately not quoted back: the backend reports the first character it could not use, so a == arrives as = and a help echoing it would read (a ?? 0) = ….macro, code written as data, and something attached to a declaration — and keeps the problem it names: generating code from a type’s shape. That needs none of the three, because comptime plus a bound is enough. Before reserving the words, the question was put the other way round: what does this language actually need, now that it has comptime?inline for over @typeInfo(T).Struct.fields. The absence is deliberate there, on a readability argument — nothing is generated behind the reader’s back. D went the other way and shows what not to take: mixin compiles a string built at build time as if it were source, which is maximum power and the best-known readability complaint about the language — and is Part III C.1’s class, messages pointing at text nobody wrote.fn describe[T: Struct](value: T) — T: Struct is what makes T.fields exist, the way any bound makes a method exist (ADR-078), so the trait system does the navigation Zig does with a union tag. No @typeInfo, no builtin at all, and @field(v, f.name) becomes field.of(value), a method rather than syntax.anytype is the most-named weakness of its build-time system — the signature says nothing, so a caller learns its type does not fit only when something deep in the body fails, with the message pointing into somebody else’s code. The bound moves one whole class of failure to the call site and leaves inside only what is genuinely per-field.T.fields is known while the program is built, so a loop over it cannot be anything but unrolled. The record states why this is not the fault ADR-082 removed: there the operand changed meaning with its position; here the loop means what it always means and only the stage differs.field.of(value) is a String for one field and an i32 for the next, so there is no single type to check it against once. At run time this costs nothing — what is left is the calls somebody would have written by hand. At build time it is fields × the types actually used, and an ordinary loop over a run-time list is checked once, as always. The cost that matters is the message, which must name the unrolling: = unrolling T.fields for User, at field avatar. Without that line this is the error class C++ templates carried for twenty years.--overlaps, --sharing, --trust — so --comptime prints what was unrolled, for the types actually used.macro, quote and with are reserved and are not constructs. 35 words become 38. Reserved although the language does not want them, on ADR-071 D3’s polarity — un-reserving is the free direction — and because all three are words a reader from another language will type, where a reserved word can carry a message and a name cannot.u.age > 18 as a tree for a provider to translate. It is the one capability left that would be genuinely new, and the record’s job was to make sure it is the only one rather than one of four.macro left the escape sweep. tests/reserved_below.rs walks the words Rust reserves and Nikaia does not, checking each is escaped rather than refused — and macro was one of them. It moves to that test’s own RESERVED_HERE_TOO, beside trait, which is the mechanism the test was built with. It stays in the emitter’s list, because that list says what the language below reserves, which is still true.quote as an ordinary name, which was the point: let q = quote { 1 + 1 } parsed as a name beside a block and lowered in silence, and NK1117 was what caught it. The grammar catches it now, earlier and better, so the fixture uses an ordinary name and the comment says why.trait as well as the three added here. It is regenerated from RESERVED_WORDS and compared mechanically: 38 against 38.[T: Struct] works. Running the example says something earlier and sharper: error[NK1117]: nothing declares T — because a type is not a value here, so T.fields reads T where a value stands. That is the deepest of three missing pieces and it was not in the list; the second is that Struct is not a trait anything declares, and only the third is the build-time loop. The note now carries the compiler’s own output, and says the bound mechanism is built while this particular bound does not exist.quote, and docs/spec-promises.md had three rows and an open question about whether these pages should exist — the question is now answered, and it was a removal.Some(…) was keyed by the parameter and not by the call. let r = pick(1) + pick(null) came out pick(Some(1)) + pick(Some(None)) — a double wrap around a value that already was None, which rustc refuses about a file nobody wrote (Part III C.1). The key was (statement, callee as written, argument position), and the checker’s own doc comment stated the design in the words that contain the bug: “a statement may hold several calls, and one call may pass several arguments — so the callee’s written name and the index together say which.” They say which parameter. A statement that calls one function twice had one entry for two arguments, and the last one walked won — so either argument could get the other’s answer, depending on the order they were written in.hold(P { x: 2 }) + hold(P { x: null }) was P { x: Some(2) }, P { x: Some(None) }. A statement may build two literals, and the key was (statement, field name).f"…" hole, which is text until each pass parses a tree of its own; and the order the calls are walked in would make the two passes agree by assumption rather than by construction. So it is structural — the argument itself, rendered the same from either pass, with the shared interner making an identifier render identically even in a re-parsed hole. Two textually identical arguments share an entry and want the same answer. The struct key also gains the type, because P { x } and Q { x } in one statement are two shorthand fields the value cannot tell apart.1 + 7 and 2 + 9).null in the second hole of an f"…"” — where it was met, not what it was — and gave a condition (“two holes with a non-null in the first”) that a probe two lines longer would have replaced with the real one. The record stands as written and [#225](https://github.com/Nikaia-Language/Nikaia/issues/225) carries the correction beside the closed entry, because a narrow characterisation is a guess about the cause wearing the clothes of a reproduction, and the guess was what made it look like a string problem for three commits.{ begins — and its D4 immediately made an exception to it, leaving ?? out because no program had run into it. Asked what else the head refused, the answer was not one level: a ?? 0, a as i64, null and (a, b) were each a parse error in the position Part I 3.2 describes with no restriction at all.null is what says which kind of defect that was. It is not an operator or a precedence level; it is a literal, in a head that already took false. A head that takes one and refuses the other is not a restriction anybody could deduce — it is a list that was copied and came up short.??, range, ||, &&, comparison, +, *, as, unary, postfix, primary — with null, a tuple and dsl … from … among the primaries. What head_primary drops is a list short enough to write out: struct_lit, block_expr, if_expr, match_expr, overlap_expr, a dsl … { … } eod, and spawn, whose lambda is a brace.if p == (P { x: 1 }) { … } parses, and did before. paren_expr takes a whole expr, so the restriction stops at the outermost token. It is not “these constructs are unavailable in a condition” but “the token that opens a body is reserved for the body” — a syntactic necessity with a one-character way round it, which Part I 3.2 now shows instead of listing operators.dsl … from … goes last: at the front it was a failed keyword match ahead of every primary in every head, and it took the headline of a parse error with it — if { } answered “expected one of !, -, dsl, false, true, identifier”, leading with the rarest of the six. null goes after the literals rather than beside false, worth 0.45 % of a compile on a head-dense file.head_cast and the new primaries are tried per operand, not per head. +0.5 % to +1.2 % of a compile on real programs. Accepted for D3’s reason: what is bought is not four constructs but the end of a class of surprise.a_head_parses_what_a_body_parses lowers the same expression in both positions and compares the two lowerings, so a level that mirrors its counterpart badly fails as readily as one that is missing. It caught this record’s own first draft, which assumed a ?? 0 > 3 is (a ?? 0) > 3. It is a ?? (0 > 3) — in a head and in a body, because ADR-066 D4 put ?? above comparison. That reads oddly and is not the head’s business; it is on [#214](https://github.com/Nikaia-Language/Nikaia/issues/214) as a suspicion, with what would have to be true for it to be a defect.null in the second hole of an f"…" is emitted as Some(None) — a double wrap rustc refuses — where the first hole, a lone hole, and the same call outside a string are all correct; reproduced identically on the previous build, so it predates this. And the measurement method turned one up on itself: the first baseline for D5 was wrong, because the script removing the head’s new alternatives matched the first occurrence of the same text in primary_expr. It produced a binary that could not parse a tuple anywhere, and it produced numbers — a −92 % that was a parse failure wearing a percentage. The real baseline is git show 5a1e99a:…/parser/mod.rs, and §3 of the record says so where the next person will look.&& and || in the head of an if, a while and a for)if a && b { … } was a parse error. The head of an if, a while and a for is a grammar of its own, and it exists for one reason — the { there opens the body, so a brace-led form may not stand in it — but it had been built by copying the ordinary chain and stopping at head_cmp, and what stopped there was not only the brace-led forms. Part I 3.2 writes a condition with no restriction on what may produce one, and && was in every position but the one a condition is written in.{ begins. A head that differs in any other way is a trap rather than a restriction — the program reads as valid Nikaia and is refused for a reason no rule states, or, worse, is accepted and means something else, which is what the missing while rule once did. The chain is now a copy with one difference, in head_primary.{. Neither connective can begin a block and neither ends an expression — after each comes another operand, drawn through the same chain from the same brace-free primaries — so the token before the body’s brace comes from exactly the set it came from before.?? is the one level the rule still reaches and is deliberately not taken (D4). a ?? b cannot begin a block either, so the brace reason does not exclude it; what excludes it is that no program has run into it, and adding a construct on a principle alone in a record whose other half is evidence mixes two kinds of change. Waiting is free for ADR-050 D7’s reason: widening what parses breaks nothing, narrowing it does.while that stops mid-body costs −25 % with a break. The baseline it was measured against had to nest an if inside its loop because this gap existed. The same benchmark, with the shape that is now writable: a flag with && in the head costs 6,500,940 and the break costs 6,501,105 — the same, to 165 instructions over two million turns. The −25 % was the nesting, not the jump. ADR-084’s decision is untouched, because it never rested on that row — what decided it was the for row, where a loop cannot be stopped at all and the gap is unbounded in n, and break-continue-cost.md §7 had already written that leading with the percentages “would have made a weaker case out of stronger evidence”. The benchmark keeps the old shape beside the new one rather than replacing it: a benchmark edited under a published number is a number nobody can check.break-continue-cost.md §2.1 rather than quietly fixed.if statement’s head is parsed twice. 2,000 while heads cost 2,536 instructions each and 2,000 if heads cost exactly 5,072, because assign_stmt comes before expr_stmt and parses the whole if … as a candidate assignment target before failing to find a =. The ordering is load-bearing, so this is a thing to know rather than a line to change.loop)while true { … } a shape somebody would write — until it had a break, the only way out was a return — and the emitted Rust then said “denote infinite loops with loop { … }” about a line nobody wrote, with a help naming a keyword this language does not have. ADR-084 §3 recorded it as a defect and named the obvious fix, one #![allow(while_true)] in the emitted preamble. That was the wrong remedy and this is the right one: a while whose condition is the literal true is emitted as loop { … }.while true { … } is this language’s unconditional loop. Rust’s name for the unconditional loop is loop. So loop is the translation of the program and while true a transcription of its letters — and the letters were what the language below objected to. The literal only, never a name that happens to be true: the equivalence is about the written form, so while running { … } stays a while even where running is bound to true.allow is the answer only after that question has been asked and answered no, and then it belongs as near the construct as it can be put.while true { } is () below and loop { } diverges and is ! — so fn a() -> i32 { loop { } } compiles and the while form is an E0308. #183 wants a function that never returns to stop needing an unreachable return, and no checker change could have delivered that while the lowering emitted the form rustc refuses: the checker would have stopped refusing and rustc would have refused instead. That entry loses its blocker and keeps only its checker half..nika in examples/ and tests/samples/ lowered and compiled, and while_true appears nowhere in the corpus. Three tests — the lowering, the name-that-is-not-the-literal, and that the loop still means what it meant, which is run rather than read.catch { … } whose handler never mentions the error emits Err(error) => …, so rustc says “unused variable: error” about a name the emitter wrote — the author wrote catch { 1000 }. That is the same class and it is on [#221](https://github.com/Nikaia-Language/Nikaia/issues/221) with its reproduction and the over-approximate walk that would close it. The other two, unused variable: x and k, name bindings the user wrote: actionable message, wrong file, a smaller and different problem.break and continue)return that leaves the whole function. It named the two missing words and did not decide them; ADR-071 reserved them so the question could stay open for free. This answers it: break leaves the innermost loop, continue starts its next turn, and the reservation is what makes the addition cost no program a name.docs/history/break-continue-cost.md is the measurement in full and the record’s evidence; everything the record decides about boundaries, about a catch handler, about rule placement was found by building it and would not have been in a design document written first.while that learns it is finished mid-body and −1 % on a continue are numbers a language can live without. The for row is different in kind: a for could not be stopped at all — return leaves the function and a flag stops only the body — so at n = 2 000 000 the shape that replaced a break took two million turns to do 1 415 turns’ work, and the gap is unbounded in n rather than a constant factor. A loop that cannot stop is not a slow loop; it is a loop that has to be written as something else.break x is refused (NK1133, D3). Rust’s break carries a value out of a loop and this one carries none, so a value after it parses as a second statement — the program compiles, the value is dropped, and nothing says so. That is the three statements, no error the while rule’s comment records from the day while was missing, and it is worse here because the author wrote something that means one thing in the language beside this one.NK1132, D4): a lambda is a Rust closure, a task an async move block (ADR-055 §6), an overlap branch one async block per branch (ADR-050 D2), and a DSL fold’s init/step/merge closures handed to the driver. The message names which of the four stands in the way; without it the answer is rustc’s “break outside of a loop” about a file nobody wrote (Part III C.1). The restriction worth knowing before writing code is the lock doors (ADR-065), whose block is a lambda — and there it is doing work, because the alternative is a lock held across a jump.catch handler is not one of them (D5), because it lowers to a match arm. D4’s table makes the opposite look like the rule, so this is a decision with a test that runs rather than an omission from a table.spawn, because a task’s body is a detached context everywhere else it is asked about. The fix is not a better walk — Flow gained in_loop, true in a loop’s body and false in Flow::PLAIN, where a lambda, a task, an overlap branch and a function body each start. Every statement is emitted through one place. ADR-055 §6’s division applied again: the checker is the diagnostic and the lowering is the guarantee, which is the general answer to “is this walk complete?” — a question no walk can answer about itself.assign_stmt, they are tried and fail for every statement that is not a jump: ~740 instructions a statement, +1.28 % on a statement-dense file. Placed last, what reaches them is only the } that ends a block, where the whole alternation fails anyway: 665 instructions per block, +0.13 % to +0.32 % of a compile. It costs nothing in correctness, because both words are reserved and no earlier arm can swallow a jump. The comment at the rules carries the reason, because an ordering with no reason attached is one the next person tidies.loop stays reserved and unused. Its condition for reopening is a break that hands back a value — while true whose break hands back a value reads as a lie — and D3 refuses one. What does change is that while true { … } becomes usable: until now it could only be left by return, which is why no .nika file in the tree writes one.catch handler can return, because what follows is then conditional on it having succeeded. A handler that jumps makes it conditional the same way, so diverts counts break and continue — and is exact about the one case where it must not: a jump bound to a loop written inside the handler lands in the handler. A rule written ahead of the case that needs it, because an analysis should be right about what a construct means rather than about which constructs happen to reach it.while true puts a rustc warning on a generated line (not caused by this, made common by it, and its one-line fix is a decision about every file this compiler writes); a grammar fold’s three lambdas are not checked at all, so an undeclared name in one reaches the backend — D4 closes only the half a jump can reach; and && and || are missing from the head of an if, a while and a for, found by writing while i < n && running, the very workaround a language without break needs.return after a while true is cheap here was “because break and continue do not exist”. The test is now “the condition is the literal true and no break in the body is bound to this loop” — still small, because the checker already keeps the loop count that answers it, and no longer the sentence that was written.loop; NK1132 and NK1133 join Part III C.3’s table, whose Status range had also lost NK1125 since it was written; tests/samples/jumps.nika is the shape Part I documents, walked by the parse, type-check and editor-grammar corpora; and the VS Code grammar scopes both as keyword.control.flow beside return — keyword.control.loop is the word that opens a loop and these two leave one.NK1131. return self.username out of a &self method was error[E0507]: cannot move out of self.username which is behind a shared reference — about a file nobody wrote, and that is every accessor over a field that is not a number, since &self is what Part I 4.2 writes for a method that only reads..clone() the emitter writes. That was the other option and it is against a decision already made: ADR-064 D2 wrote a hull you can see is one you write, and a copy the author cannot see is the same thing one position over — an allocation in the hot path that nothing in the source accounts for, in a chapter whose whole argument is that a value’s cost is visible. It would also buy nothing: both ways out are one word and the message names them..clone(), and a self receiver. The rule reads the field’s type rather than counting &selfs, and the positions are the three that were measured: a position missed leaves today’s behaviour, and a position wrongly included breaks a program that works (C.4).arguments_given — found by the class measurement coming back two-of-three instead of three-of-three..clone() had no ledger entry, so sync::infer could not vouch for the call and the method around it came out as one that pauses — at which point ADR-080’s NK1129 refused it for disagreeing with its trait. Taking NK1131’s advice made a different diagnostic fire. Found by the trait tests’ own fixture, which was also the first program the new refusal caught writing return self.name out of a &self method — a program that had never compiled. String::clone and str::clone are in std.contracts now.&str for text, &Vec[i64] for a list. The other two each take something: an allocation, or the whole subject.fn name_of(&self) -> &str { return &self.name } compiles and runs — the & is Part I 6.5’s own spelling (let name = &config.name), and NK1104’s help had been saying “write & to take a view of it” on the same screen. What is refused is the same line without the &, which is a String in a &str slot: NK1102 at a parameter and NK1103 at a let, so the rule is uniform and the accessor is not special. The E0106 the filing also claimed came from rustc reading a stale file, since a refused program writes none. Both were a minute’s work to check.dsl <grammar> from <expr> for an ordinary call — Json.value(input). The block form, dsl <driver> { … } eod, is untouched: its body must not be read as Nikaia at all, which no call can arrange, and that is what earns it a keyword.dsl spelled two constructs with nothing in common, and took its name from two different places. dsl Json from x looks Json up among the file’s grammars and refuses if there is none; dsl mysql { … } eod does not resolve its target to a grammar at all — Part II 10.2’s own status note says so — because mysql names a driver. One keyword, two namespaces. After this, dsl takes a driver name and only a driver name.dsl Json from "config.json" parses the eleven characters config.json; in a build-time binding it was meant to parse that file’s contents. The specification presented that as a feature — “the same syntax … follows from the context, not from a different spelling” — and it is the one thing this project refuses everywhere else. Now three visible steps say it: comptime when, from "…" where the bytes come from, the call what is done with them.pub rule is an entry, and the distinguished one was ours. winnow-grammar generates one function per public rule named after it (Cron::parse_schedule()) and has no main rule — the pick is this compiler’s, and it is find(|r| r.is_public && par_fold_of(r).is_some()).or_else(…): the first public rule wins, and a par_fold one beats an earlier one, by source order, silently. The call form removes the question rather than fixing the pick, and with it the refusal “grammar … has no pub rule to enter through”.parse alias. One that exists only while a grammar has exactly one public rule is a name that appears and disappears with its neighbours; one a grammar must declare says a second time what the rule name already says. A program that wants Json.parse(x) names its rule parse.+ over text is a call, and an expression gets a span to key it by)String + &str rule reaching the user unchanged, about a file nobody wrote. And the checker got it wrong first, which is the worse half: "a" + s came to a &str, so a function declaring -> String was refused as NK1104 — a false refusal, and Part III C.4 says this compiler never refuses a correct program. Part I 4.7 wrote that line as its own example.Expr::Binary carries a span, the first expression-level one in this AST. contracts/sync.rs has called them open work in its own words since it was written. It exists because the answer had to be keyed by which + rather than by the statement around it: a + b + c is two of them, and every other answer in ADR-028’s channel is keyed by a statement because every other answer is about one thing per statement. One variant and not every expression, because a field nobody reads is a field that goes stale.nikaia_std::concat::plus, and the result is a String. A trait with an impl per shape, which is ADR-048 D1’s own sentence a third time — and it costs nothing: three million concatenations take 42 ms as String + &str lowered before, 250 ms through a format! for every shape, and 42 ms through the trait. The single obvious rule would have been a sixfold regression on the one form that already worked; the rule turns out to be the table of four cases, written once in std instead of four times in the emitter. The String is a decision rather than an inference: a concatenation makes a new value, and saying &str is what made -> String a false refusal.+ does not move, and that is what keeps D2 narrow. Arithmetic inside nikaia_std would silently lose ADR-043 D1’s overflow abort — overflow-checks is on per Nikaia crate and off for the profile, because a foreign crate’s hash function wraps on purpose. Measured: a + b in the checked crate aborts, and the same a + b through an #[inline] helper in the unchecked one wraps silently to -9223372036854775808. Inlining does not carry the check across; it is decided where the code is written. Written down as a trap for the whole index::at family rather than left to be met again — that one escapes it only because its conversion is an explicit try_from.println(prefix + "System started") compiles, and the specification sweep’s compile baseline moved one line from error[E0369] to compiles.&self method cannot hand back a field it owns (E0507), which is every accessor over a String or a Vec and has nothing to do with text. So the page keeps its interpolated form, and closing that entry is written down as the condition for putting its own sentence back.format! for every shape as one rule rather than a table of four cases, at the cost of an allocation where String + &str reuses today. A trait in std with an impl per shape — the arrangement ADR-048 D1 already uses twice — makes the rule be the table: three million concatenations take 42 ms as String + &str lowers today, 250 ms through format!, and 42 ms through the trait. So format! everywhere would be a 6× regression on the one form that works, and the trait is free.+ would take numbers too, and a number’s + may not leave this language’s own crates: ADR-043 D1 turns overflow-checks on per Nikaia crate while the profile turns them off, because a foreign crate’s hash function wraps on purpose — and nikaia_std is foreign to that split. Measured with the same profile shape: a + b in the checked crate aborts, and the same a + b through an #[inline] helper in the unchecked crate wraps silently to -9223372036854775808. Inlining does not carry the check across; it is decided where the code is written.+ is one. That needs a key, and Expr::Binary has no span to be keyed by — which contracts/sync.rs already names as open work in its own words.index::at and index::set are unaffected and it is worth saying why: the first converts with an explicit try_from and the second does no arithmetic at all. The trap is specific to moving an operator into std.< /dev/null fails identically three runs out of three, so the caller’s stdin decides nothing.--print output now arrives complete, and what fails is compiling the source on stdin — which turns out to be a rustc diagnostic about a malformed #[diagnostic::on_unimplemented] attribute. No such attribute exists anywhere in this repository and the string is rustc’s own, so it is not something this compiler wrote or relayed.error:, --> and caret lines above it. So it is another process’s stderr crossing into a probe’s stdin, rather than one stream being cut short — which is what the first two sightings looked like. The producing crate is not identified; serde carries #[diagnostic::on_unimplemented] in the graph but in the form this toolchain accepts.origin/main built from scratch fails identically; in the passing window it passes. Ruled out so far, in order: the tests, the wrapper’s own code path, a stale binary, a rebuild immediately before, and the caller’s stdin.impl owes its trait, and a write through the brackets is not an index)NK1130: an impl and the trait it names disagreeing about which methods exist. ADR-078 made the declaration possible and said in its own §4 that nothing checked against it, so a method the trait does not declare, or one it declares that the impl leaves out, went to rustc as E0407 and E0046. Two messages under one code, because a reader is doing two different things: moving a method, and finishing an impl.NK1129: an implementation that pauses where the declaration cannot say so. ADR-078 D4 asserts sync for a trait’s methods and had to — No would have made every call through a bound an .await. An implementation whose body pauses therefore lowered to async fn against a plain fn, and the language below answered “method load has an incompatible type for trait”. Refused here, because async fn in a trait is a thing this emitter has no way to ask for; the way to allow it is a word in the declaration that says a method may pause, and this language has only sync for the opposite.impl Error for ConfigError names the one trait the compiler reads rather than one a .nika file wrote (ADR-023 D3), and silence is the only correct answer about a declaration that is not here.sync column is why. The type checker runs before sync::infer — that order is what keeps ADR-027 sound, since the checker resolves the calls the inference reads — so a rule needing the finished column runs from check_program beside dsl::check and views::check.scores["Player1"] = 100 failed with error[E0282]: type annotations needed for HashMap<_, i32, BuildHasherDefault<FxHasher>>, in a message naming TrustedMap, BuildHasherDefault<FxHasher> and a type parameter K — three spellings the program never wrote, which is Part III C.1 at its worst because it names this compiler’s own internal word for a map.Index for a map is over whatever the key borrows as, so indexing with a &str is satisfied by String, &str and Box<str> alike and pins nothing. insert takes the key by value and pins it exactly — so a write through the brackets lowers to nikaia_std::index::set, which is insert for a map and the indexed assignment it already was for a sequence. A trait and not two emitter cases, which is ADR-048 D1’s own sentence a second time: this emitter does not know types, and xs[0] = 5 and scores[k] = v are the same three tokens.=. A compound m[k] += 1 reads the slot as well as writing it, so it is an Index either way — and what reading an absent key means is a question of its own.error[E0282] to compiles, which is what that baseline is for. Its report is also addressed by a block’s ordinal now rather than by its line: a line number moves whenever a paragraph above the block gains a sentence, and one such edit churned twenty-one lines without changing a single verdict.Vec[Row] parses perfectly well and has nowhere to land.Vec[T] as &[T], String as &str. Not a new distinction: it is the one Part I already makes between a buffer and a view, at the place where it is forced rather than chosen. It also coincides with where the language below draws its own line (const X: &[T] exists, const X: Vec<T> cannot), so the two boundaries need no negotiation. D2 refuses what owns memory by its type rather than by its parse, and rejects the alternative of building it once at startup — that gives up what the feature is for.ROUTES.get(name) is what a program writes, and whether that is a perfect hash, a binary search or a match is the compiler’s, exactly as ADR-010 already picks a hasher from key provenance with nothing in the source mentioning hashing.PerfectHashMap, no build() to call. The measurement is the argument: over 200 keys a match beats a hash map 9.0 ns against 18.0 — and that is a statement about 200 keys, not about a million. A decision whose right answer moves with the size of the input belongs where the size is known, and that is not the source. A program may say this map is fixed; it may not say how the compiler should honour that.repr the program never asked for, and padding bytes are a hazard to ADR-005 D8’s CI-enforced byte-determinism. And emitting a literal already is serialisation, done by the linker, which knows the target and needs no unsafe cast.std, an evaluator that can loop and push (which is the real work), and — for the case where somebody writes the pairs by hand rather than parsing them — the list literal, which Part I 4.5 already records as unbuilt. Which has the odd consequence that the toy version of this feature is harder to reach than the real one.origin/main, three runs out of three each — but a third sighting showed rustc being handed, as source at <anon>:7:55, a line that is a rustc diagnostic. <anon> is standard input, which is where cargo puts the probe’s source, so something other than that source was on the wrapper’s stdin.cargo test --workspace many cargo instances probe at once and share one. Two readers on one pipe would take each other’s bytes, which is the shape of both symptoms seen — a truncated read and a read of somebody else’s output. Recorded as a hypothesis with evidence, because the next step is to measure it rather than to change the wrapper on a story.docs/README.md §1 already makes a stale Status note a defect in its own right; crates/nikaia/tests/specification.rs applies the same rule to the code beside it. It takes every nika block in the three pages as far as it goes — parse, check, lower — and hands the ones that lower to rustc, against two recorded baselines.User, a Config, a postgres or an http the chapter around them declares, which is a fragment rather than a defect: NK1117 deliberately does not refuse a name this compiler cannot see (Part III C.4). The baselines are what tell the two apart without anybody re-deciding it each time, and a block that stops lowering — or starts — is a line in a diff.main, a body wrapped in one, and items followed by statements — the last being the shape a chapter uses most and the one neither obvious reading can take. A block with an elision where code would be is marked a sketch and not counted against anything.return "User: " + self.username was refused twice over — the checker said it hands back a &str where String is declared, and the lowering would not have compiled either — and three plain strings in Part I 7 held {p}, {line} and {expected}, written before ADR-035 D5 made only f"…" interpolate. A test now asserts that Part I 2.5 is the only block allowed to trip NK1111, because that section is about the difference.String + &str compiles, so Rust’s own rule reaches the user unchanged — and the checker types "a" + s as a &str first, which makes a function declaring -> String a false refusal. And Part I 4.5’s three-line map example fails with E0282 in a message naming TrustedMap, BuildHasherDefault<FxHasher> and a type parameter K: three spellings the program never wrote, which is Part III C.1 at its worst. #207 and #144 have the measurements and what each would cost to close.about-a-refusal, which is what it detects, and the third test counts how much of the specification is a program instead — a floor to raise, not a property to assert.trait Summarize { … } as its own example and it was a parse error at the keyword: a trait could be implemented and not declared, so the traits a program could implement were the ones std and the Rust side already name. That lived in a Status note and nowhere else — and a Status note is not the former backlog file, so the item the specification’s own example needs had never been ranked against anything. ADR-074 §4 made it load-bearing: a bound names a trait, so [T: Summarize] had nothing to name.ast::TraitMethod has no body — not an Item::Fn with an empty block and not one whose body became an Option, because the difference between “declares this method” and “this method does nothing” is the whole of what a trait is, and a body that is absent cannot be accidentally emitted. The emitter gets a second, smaller writer for the same reason: function reads the ledger for sync, for each position’s Shared count and for whether the call can fail, and every one of those is an answer about a body.Summarize::summary — the same key shape an impl’s methods get — so a member reached on a bounded parameter is looked up by asking the call again on the trait. The bound therefore answers the whole call: arity is NK1101 from the declaration, argument types NK1102, the result type is what a let or return is checked against, and sync, throws and the signature’s type variables come from the same entry. NK1126 is unchanged where no bound declares the member, which is what keeps it honest — a bound is not a blanket licence.[T: Summarize], and [T: A + B] for several, written where Part I 4.6 already puts the parameter.sync in the ledger, and that is a decision. sync::infer raises a function from No to Inferred by reading its body; a declaration has none, so No would stand — and No means pauses. Measured: fn shout[T: Summarize] came out as async fn shout awaiting a String. Asserting it is the only answer this position can write, because async fn in a trait is something the emitter has no way to ask for. Trait methods also join sync::infer’s graph as unblocked leaves, since a callee absent from that fixpoint was read as one that pauses.trait is a reserved word now, which it was not — ADR-076’s sweep found it as a name, because let trait = 3 was a legal program.User: Ada, which with ADR-074 finishes chapter 4’s generics section end to end.traits was the one map absorb_renaming left behind — visible only as a third resolution pass with an empty map where the first two had it. Then the lowering awaited a String, which is D4. Then the fixture itself did not compile: Part I 4.7’s "User: " + self.username is &str + String, accepted here and refused below. Both that and a trait whose method genuinely pauses are in the former backlog file with their reproductions, named as choices rather than repairs.impl against the trait it names, so a method with a different signature — or a missing one — is refused by rustc rather than here. That is the next piece and it is a refusal, which costs nothing today. A default body has no syntax, a trait is not a type, a bound cannot stand on an impl’s own parameter, and a trait cannot be named from another package.comptime)const was taken without an argument because it is the word every neighbouring language uses, and it is wrong here for a reason only this language has: Part I 2.1 already makes every binding immutable unless it says mut, so a keyword meaning this one does not change distinguishes nothing. What the declaration promises is a time — evaluated while the program is built, refused if it cannot be — and comptime is what says that.const either — and calls its immutable binding const, which is the opposite assignment, for a language whose default is the other way round.const stays reserved rather than released (D2), on ADR-050 D7’s polarity: un-reserving is the free direction and reserving is not. It is the first thing a reader arriving from another language will type, and a reserved word can be given a message where a name cannot — `const` is a reserved word, so it is not a name. The list goes from 33 to 34: one keyword more, not one instead.const, because that is Rust’s name for the same slot: comptime LIMIT = 4 * 1024 arrives as const LIMIT: i32 = 4096;. The test that covers the lowering now says which word is on which side, so it does not read as an inconsistency.NK1127 keeps its number and changes one word.cargo fmt since it was last edited, so a replacement matched nothing and passed silently. The specification’s table and RESERVED_WORDS are compared mechanically again — 34 against 34.let type = 3 lowered to let type = 3; and rustc answered “expected identifier, found keyword type” about a file nobody wrote, with “escape type to use it as an identifier” as the help — advice that means nothing in this language. Part III C.1’s class, and this compiler had it in every position a name can stand in: a local, a parameter, a field, a function’s own name, a struct, an enum, a variant, a lambda’s argument, a for binding.type becomes r#type in the generated file and stays type in the source, in every diagnostic and in the ledger. Reserving the twenty-seven words instead would let the backend decide what this language’s vocabulary is — type is the field name of every tagged record anybody has written, and move, ref, static and final are ordinary words. ADR-051 D1’s reserving costs nothing before programs exist is about words this language needs for its own constructs, and these are not them. The cause is ADR-011 D2 writing a name for a name, so the fix belongs where the name is written — ADR-056’s a rule and not a list at the same boundary one position over.text. One line there would have covered everything at once and been a silent defect: text is also what the checker’s recorded answers are keyed by — pausing_methods, fallible_methods, nullable_fields all keyed by the name the source wrote — so escaping there would make every one of those lookups miss, on exactly the programs this is for.NK1128: three names are refused, because there is no escape to write. Rust’s own reply to r#crate is “crate cannot be a raw identifier”, and the same for super, self and Self. So the one rule has one forced exception and it is the target’s rather than a choice made here; self keeps NK1119, which says more. Three words rather than twenty-seven is the point of D1: crate and super name a module tree this language does not have, and Self is already how it writes the type an impl is on.NK1119’s walk never covered an item’s own name, because struct self cannot parse — and struct crate parses fine and went straight through.let W = 3 and read rustc’s “found keyword”: twenty-four words. It missed crate, super and box, which fail there with E0532, E0433 and “expected pattern” instead. Twenty-seven, found by compiling every word in every position and asking only whether the program builds. That sweep is now the test, and it is what proves coverage rather than arguing it — an escape applied at the position that declares a name and missed where one is referred to is worse than no escape at all. Two of its own first versions failed for reasons that were the test’s (a local shadowing a function of the same name; a let that needed let mut), and both are written into it.struct Event { type: String } compiles, runs and prints what was put in it, which is the program this was worth doing for.const is a declaration, inside a body)const LIMIT = 4 * 1024 parses, is evaluated while the program is built, and reaches the language below as const LIMIT: i32 = 4096; — the arithmetic does not survive, which is the visible half of D3. Six tests cover it, one of which compiles and runs the program rather than reading the emitted file.let may fold; a const must, and says so where it cannot — NK1127, whose help names the way out, let, for a value that was never a constant. Refused rather than computed later, because a guarantee that silently degrades into it happened to be cheap is worse than one that is absent.const takes a type and this emitter has none (ADR-028) — but it also has no scope, and folding const PAIR = PAGE * 2 means knowing what PAGE is. Without the value travelling with the type, a constant built out of another would reach Rust unfolded and D3’s demand would be rustc’s to keep. A test covers exactly that shape.true or false. A call is the second stage, so dsl … from "…" still has nothing to stand in.return now appears in the also possible here list of two parse errors, because the statement rule gained an alternative. Strictly more informative, and regenerated rather than hand-edited.const had also drifted to 074 and points at ADR-073 D1 again, which is where the word is reserved.cat > does not ask.scripts/check-adr-refs.py now checks that every number belongs to exactly one record, which is the part a machine can keep: one index row per number, one file per number, and a file whose own title agrees with its name. Every citation still resolved while this was broken — the file was there and still had a D1 — but the index carried two rows for 074, and that is what is now read. Confirmed by reintroducing the collision: the check fires and names the number. CI runs this script, so the next one goes red in minutes rather than surviving three commits.--overlaps printed “which meet on nothing” as a fixed header over every block, without consulting a single verdict — so a block the checker refuses on the very next line was described as meeting on nothing. That is the one answer this tool must never give: it exists because “the refusals are the compiler’s own … that is only fair if the refusals can be asked about”, and a reader consults it precisely when the checker has said no. It now runs the same pairwise verdicts check runs and names the first refused pair and why — branches 1 and 2 may not run together - both reach stdout, and one writes it. The test covers both directions, so a fix that hard-codes the other verdict fails it too.sync entries in std.contracts had no touch answer (ADR-075 D2 needs one, #205 listed them), and fail-closed that reads “touches everything” — HashMap::new, HashMap::get, f64::sqrt, Vec::push, str::trim among them. Twenty-three are touches = []: they compute over values the caller already holds and reach no resource in the vocabulary, which is what the file’s own note beside the saturating-arithmetic entries already says about a pure computation.fs::Mapped::deref is the one that is not, and it gets touches = ["file read"] with the reason beside it: a mapping is memory that is a file, so reading through it reads the file — and which file is the mapping’s rather than a parameter the line could name. An unnamed read conflicts with every file touch, which is the safe direction; [] would have claimed an fs::write to the same file may be reordered across it."abc".len() — measured by running the same program with and without them. They are what ADR-075’s build-time check will read, and HashMap::new is the one of the twenty-four the ordering pass can already use.sync means exactly one thing and touches is inferred over the call graph, so the restriction is maintained by the compiler rather than by a list.std’s 74 sync entries, ten touch something: cli::args and Args::nth read the arguments, print/println and eprint/eprintln write, and the four lock doors take locks. So “anything sync” alone would let a build-time body read the environment it was invoked in and write into the compiler’s own output — which is what makes the second column necessary and the exact boundary a decision.args read. cli::args passes deliberately — same parameters, same code — because a build’s parameters are an input the way its source is. println is refused as a consequence of the boundary rather than a judgement about it: a build-time print is a reasonable thing to want, build.rs has one and Zig has @compileLog, and it is a later decision.Choices holds the build shape and the backend and nothing else — and its own doc comment says what to do about it: one field, one line in the key. Skipping it would make the cache serve a build made with other parameters. And cli::args stays one function with one meaning, the arguments of whoever is running me.sync says a body never pauses, not that it ends, so a const that does not terminate hangs the compiler silently — which ADR-026 Q6 already called worse for the local developer than one that stops and names the call path. Deferred rather than decided against; a budget added later refuses only programs that were already hanging.wasm32 target required of every contributor and every CI job, and no asterisk on determinism for NaN bit patterns, which Q3 flagged as able to make two builds both correct and different.sync entries have no touch answer at all, so fail-closed they read “touches everything” — HashMap::new, HashMap::get, f64::sqrt among them. Under D2 a const could not use a map. Twenty-four missing lines, not a design problem; the inference cannot fill them because it walks Nikaia bodies and these are backed by Rust.struct Box[T] since it was written, and the language had the form without the feature: the [T] was read by the parser, erased by the checker and dropped by the emitter, whose function header had no slot for one. So fn hand[T](x: T) -> T passed every stage of this compiler and rustc answered “cannot find type T in this scope” about a file nobody wrote — Part III C.1’s class exactly.T to ? for a good reason — a T that survives into a comparison makes the checker tell a caller that its perfectly good i64 is not T — and applied that reason to both places a parameter is read. It holds in one of them. In the body the caller has already picked what T is, so a body that puts an i64 where its caller’s T is wanted is wrong, and saying so is the only true thing to say. Self keeps the erasure, because no call site binds it.hand(n) used to be ?. The same ty::bind against one pattern per argument answers i64, and Pair { first: n, second: n } is a Pair[i64] read back off its fields. It stays one structural walk — no queue, no fixpoint, no occurs check — because a parameter is written, never inferred, so there is never a variable on the right-hand side to solve for.NK1126, is what makes writing the <T> worth anything. Emitting the parameter alone turns “every generic fails” into “every generic whose body uses its parameter fails”, which is the same C.1 class one size smaller: fn shout[T](x: T) -> String { return x.to_uppercase() } still goes to the backend. It is refused here instead — “T stands for a type the caller picks, and nothing says it has a method to_uppercase” — for a field and for a method alike. It says no bound rather than no such method because the method may exist on every type a caller will ever pass, and what is missing is the sentence that says so.impl’s parameters come from the slots it already writes. impl Stack[T] declares T; impl Stack[i64] declares nothing and applies i64; what tells them apart is whether the slot names a type. No impl[T] spelling is added, because Part I does not have one — and the rule lives in one function the ledger, the checker and the emitter all call, since two copies would be one silent disagreement about the names a method’s signature was recorded with.signature column records $T where it recorded ?, which is what lets a reader see that hand’s result is its argument.rustc: less than writing the copies out — sixty functions at eight types each compiled in 273 ms as one generic and 304 ms written by hand, within 2 % on object size; what costs 2.3× in time and 5.9× in code is having 480 bodies at all, which is the author’s instantiation and not genericity’s overhead. In this compiler: nothing measurable — the front end on the three largest examples is unchanged within noise, because the work added is one bind per call against a signature already in hand. The figure that would change that is the one nobody should pay: inference. ADR-074 §5 has the tables.[T: Summarize] names a trait, and Part I 4.7’s trait Summarize { … } is a parse error — a trait can be implemented and not declared. Until it can, a generic body may move and pass its value and nothing else, and NK1126 says exactly that.cargo test --workspace --release failing the project tests does not reproduce. It was filed with a measurement — 16 of 22 failing on origin/main with nothing applied, three runs out of three, against 22 of 22 passing when the same tests run alone. Seven consecutive clean whole-workspace runs since, two of them with a full rebuild immediately before in the same invocation, which was the best hypothesis and is now ruled out. The entry is downgraded to a suspicion rather than deleted: the former backlog file’s own head makes that difference load-bearing, and what the earlier observation is still worth is what it rules out if the failure returns — not the tests, not the wrapper’s code path, not a stale binary.const is reserved, and what it will mean is decided)pub applies to and carried a parenthesis saying the declaration has no syntax; ADR-072 decides how a compile-time read is permitted and had nowhere to put the from line.let bound to 2 * 3 is folded twice on the way through, and a name that folded keeps the type it took (ADR-063). So const adds no folding. It adds a demand: a let may fold; a const must, and says so where it cannot. A guarantee that silently degrades into “it happened to be cheap” is the kind of promise this project treats as a defect.dsl … from "…" — waits for Q4 to say what a build-time body may do. Everything in the second group is refused by name rather than quietly falling back to program time, which would break D3 in exactly the way D3 is about. A const that parsed and evaluated arbitrary code would answer the sandbox question by accident.pub to leave the package — Part I 9.2’s existing rule, whose parenthesis this removes) and inside a body; a const in an impl is deliberately not decided. D4 makes the type optional, inferred like a let’s. D6 makes it a value and not a place: no address, no mut, and no second declaration for a build-time value that has one, because nothing has asked for it.HashMap’s 18.0, five runs each on 200 keys — ×2, with the honest caveat that this is against the default hasher and ADR-010 already picks a faster one for keys that did not come from a remote peer, so the real factor is smaller. The 3.9 µs of startup it also saves is not the argument. The third thing is not a number: a malformed file stops the build instead of the program. And the line it sits past: a perfect hash over keys in the source is compiler-side staging and needs no const at all — it is the keys coming from a file that does.const is the 33rd reserved word; const X = 1 is still two statements to the parser. Part I 2.1’s table and RESERVED_WORDS were checked mechanically against each other again.const CONFIG: Json::Value = dsl Json from "config.json" — and ADR-026 D1 already decided that the compiler does that reading rather than the evaluated body. What was open was its Q5, the question with teeth: D2 keeps a build out of /etc/passwd and does not keep a dependency’s grammar out of your secrets.toml, which sits inside your project root exactly where D2 allows reading.from "…" literal, where a reader learns what the bytes are for; in an allowlist file, where a reviewer sees the whole set without reading the code; and in the invocation, --allow-read-from-list=…, where whoever runs the build says the set applies. Two of the three are committed, so adding a read is a diff; the third is not, and that is what it is for — a build can be run with reading switched off without editing anything.build.rs attack surface ADR-026 Q6 names, answered by making the read impossible to add quietly rather than merely recorded after the fact. ADR-021’s lockfile makes a new read visible after it happened; this makes it impossible before.Key::build with a comment saying nothing reports assets yet.const in the parser — at item level a parse error, in a body two statements refused as “nothing declares const” — so there is no place for a from line to stand and nothing to check. The word is not reserved either. What stays open is ADR-026 Q4, what a const body may compute: this record decides where the bytes come from and nothing about what happens to them afterwards.break, continue and loop are reserved)break and continue are not in the language at all — not in the grammar, not in the parser, not on the list — so a loop is left by its condition going false or by a return that leaves the whole function. Whether they join the language is a question this does not answer; whether the words stay available as names has a deadline, and that is what is decided.overlap while the grammar had no construct for it.loop is reserved although the language decided against the keyword, which looks inconsistent and is not: ADR-070 D2 wrote down the condition under which that no reopens — the day a break hands back a value — and the word has to still be free on that day. The polarity is settled by ADR-050 D7, which un-reserved seq and recorded why that direction is free: reserving narrows what parses and un-reserving widens it, so a reservation is reversible and its absence is not.Alt is implemented up to; twenty-eight was over it, thirty-one is further over, and the width is what broke last time.let loop = 1 already answers “loop is a reserved word, so it is not a name”.null, which the compiler has reserved all along. Checked mechanically this time: the table and RESERVED_WORDS now hold the same 32 words.while true { … } is the unconditional loop and there is no second spelling for it. Part I 3.3 carries the sentence, so the absence is a decision rather than an omission, and loop is withdrawn from the roadmap.while at all: for { … } is its unconditional loop. So Go did not do without the form — it did without a second keyword for it, by letting one word carry all three shapes. That is the lesson, and this applies it with the other word as the general one.loop keyword does not reach here. loop earns its word twice in Rust — break value makes it an expression, and the checker knows it diverges. Neither half applies: break and continue are not in the grammar, not in the parser, and not among the 29 reserved words, so a loop is left by its condition or by return leaving the whole function, and there is no value for a break to carry. The day that changes is the day to reopen this, because while true with a value-carrying break reads as a lie.fn forever() -> i32 { while true { let x = 1 } } is refused with “this function hands back (), and it declares i32“, so a function that genuinely never returns needs a return that cannot be reached. A while on the literal true cannot be left except by return — precisely because break does not exist — so the analysis is one test on the condition. It is on #183.loop, break and continue is a separate call and D4 does not make it: reserving is not adding, ADR-051’s rule says the free moment is before programs exist, and smuggling a language change into a record that says no to one would be the wrong place for it.http leaves std)std::http and promised “a production-ready HTTP/1.1 and HTTP/2 server and client”; the first chapter uses http as its worked example of a foreign package — the passage that teaches how one package reaches another at all, http = { path = "../http" } — and ADR-046 D2 reaches for use http::server as its example of “a path that is not std’s”. Nobody decided it and nobody noticed, because while there is no way to name a package by a version, “in std” and “a package” look identical from here: either way you cannot have it.examples/, reached by a path. That arm is not a promise about the future — project.rs already resolves it recursively, refuses two names for one path and names each package once per build. What the corpus exercises today is the Rust-passthrough arm in examples/foreign-runtime/; the Nikaia arm is crates/nikaia/tests/project.rs’s.std. The three parts of the design that genuinely cannot live outside a standard library are already inside one under their own names and for their own reasons: the untrusted-path rule is ADR-010 D8’s and is about provenance rather than HTTP, escaping by type is std::html, a body the program never allocated is fs::map and Bytes. What remains is the protocol and the framework — large, compiler-free, and ageing.std later is an addition; moving a module out of std is a break with no deprecation that makes it not one.std is a version of the language — the compiler reads its ledger and checks it against its own inference. A protocol implementation wants to be wrong in public and change its surface twice, and twice inside std is two breaks.http leaves examples/.docs/lock-free.md §6 where the measurement already was. The win is 6 ns a door on the values that cross a thread; nothing is half-built and no record promises it. The reason it has no urgency was found by looking: no .nika file in the tree uses spawn, Locked, Shared or SharedMut, and the one genuinely parallel example shares nothing — par_fold splits, folds per piece and merges. The question that decides it is not what the loop saves but whether a real program shares a word-sized value across threads, and writing a contending program to find out would be manufacturing the evidence. §6 also warns whoever measures it on a server: a request costs tens of microseconds against a door’s six nanoseconds, so a server settles the shape and never the number..nika file in the tree uses spawn, Locked, Shared or SharedMut. The concurrency half of the language is exercised by Rust test snippets only — 106 mentions of spawn in crates/nikaia/tests/, none in a program. The one genuinely parallel example, the One Billion Row Challenge, shares nothing: it splits, folds per piece and merges.sync means one thing, and touches gets the inference it was written for)println inside an update compiles and prints — so sync does not forbid an effect, and worse: println takes a lock, so a print inside a door is a lock taken while a lock is held, which is the nesting ADR-039 D2 refuses and the refusal is not built. Rust’s stdout lock is re-entrant, which is why nothing has hung; it does not make the nesting sound.sync means more, and the records disagree with the page while agreeing with each other: three of them use the word and every reason given is about pausing — including the panic hook, which “must be sync” because “mid-panic there is no runtime to pause on” and which may block. A property that permits blocking is not a pure CPU task. D1: one word, one promise. 12.1 and 12.2 say never pauses now and point at the second column for the rest.println out of sync was the other way, and it costs the panic hook — which must write a crash report — and makes every function that prints async for a suspension that does not happen.touches is inferred from the body, like sync, throws and sharing. It was the one of four specified with the fail-closed polarity and never filled, so every function a .nika file declared answered “nobody said” — which means “it touches everything”. Safe, and useless: the walk stopped at the first call out of std. A greatest fixpoint over the call graph, reusing the one call walk the checker and sync already share.lock joins the vocabulary, which ADR-033 D2’s table named and touch.rs kept out under its own rule — a word waits until a program asks for it, “neither function exists”. Both exist since yesterday, and a program asks. It names no parameter: the property says a lock and never which, which is D4’s decision.update reports ["lock write", "stdout write"], the nesting written down; overlap can prove disjointness past a call into user code; and “touches nothing, and it is known” is very nearly “repeating it is unobservable”, which is what the lock-free route was waiting on.pure nor repeatable is a word this language now has: pure is more than any of the three consumers need — a retry recomputes, so it wants no effect rather than the same answer — and a third promise beside sync and touches is how this went wrong the first time.fs::read touches file(path) and path is its parameter, so a caller is left unknown until an argument can be mapped onto it.lock-free.md §3). The question was whether a compare-and-swap loop is worth building; the first table said the shipped crossing lock costs 63.5 ns where ADR-057 §4 measures its owner check at +2.0 ns. Either the record was wrong or the implementation was not the thing the record measured. It was the second.Mutex, taken and released before the acquisition and taken again after — three mutex acquisitions per door; who is asking was a SipHash of the thread id on every call; and the mark was written before the acquisition, so while one task waited the mark said its name although another held the lock.AtomicU64 written under the guard now, so it does — and still read before the acquisition, because once that blocks there is nothing left to report to, and a racing read says nothing about our own re-entry, which is the only thing it answers.lock-free.md and benches/lockfree, against the door ADR-039 §3 left open: update takes a pure function and is therefore repeatable, which is the route Clojure’s atom and Haskell’s TVar took.user_parallelism = no. A borrow flag is one non-atomic write; a compare-and-swap is a locked instruction.fetch_add is ×0.54–0.57 of the loop. So the answer, if it is taken, is narrow: a third representation for a word-sized value that may cross a thread, replacing the crossing shape and never the cheap one.nikaia-std on purpose, which is the opposite of benches/lockfloor’s choice and for the same reason: those two shapes are what the emitter writes now, so measuring anything else would measure a shape no program gets.access_all reads and update writes one. Measured before deciding, and worse than a missing door — a program that wrote access_all(a, b) was refused by the rule that protects locks: “nothing written down describes access_all … a lock may not go into code nothing written down describes”. The language’s own multi-lock door tripped its own foreign-call refusal.access_all(a, b) fn(x, y) { … } since the trailing-lambda rule, so what was missing was never syntax. Not a method on one of them: konto_a.update_with(konto_b) reads as first A then B, and an order the source implies is the bug the door exists to prevent.update_all hands back one new value per lock — update’s rule widened rather than a second rule. Nothing is handed anything it may change, nothing sees either lock between the two writes, and the block stays a pure function of what it was given, which is what keeps the retry route open. Clojure’s dosync and Haskell’s TVar take the same shape of block.access_all(a, b) in one task and access_all(b, a) in another take them the same way round, so there is no cycle to deadlock in — the property 12.3 states, made true rather than described. Measured: two threads, two thousand transfers each in opposite directions, neither deadlocks and nothing is created or lost.Door trait says what a door needs of a lock, and a handle on one is one too, so a SharedMut[T] and a Locked[T] field reach the same door.sync column for both names from an entry with no signature, so a function that uses a door keeps its claim; the compiler carries the types. The guard that every from(f) entry names a lambda parameter asks the mechanism whether a name is one of these rather than carrying a copied list.NK1124, for a door handed something that is not a lock, one where several are wanted, or a block that does not name one value per lock. A number is refused there although its type says nothing — a literal carries none on purpose, and reading that absence as “might be a lock” is what sent the program to rustc about a trait bound.spawn lowered; the moment it could, the task took the handle with it and rustc refused the later use — “borrow of moved value: counter“, about the generated file, on the program Part II 11.2 is about.spawn. Unconditional, not only where the name is used again (ADR-040 D2): a line further down may not decide what a line further up does to a cleanup point.Checked::task_handles).main reads it back, and it prints 1 at both settings. At yes the value is atomic in both hulls because a task takes it; the same program without the spawn takes the cheap pair — the per-value answer, not a floor.yes)SharedMut[T] and Locked[T] are not types the compiler knows”, 12.3’s “what is not built is what they call”, Part III 15.2’s reachability rule, the panic table, the NK22xx shapes and the crossing verdict’s note that such a program “would fail to emit”. The reason has moved in every one of them: the types are there and every refusal around them is what is missing. That is a shorter way to go than it was, and the notes now say which.NK1115’s way out is the constructor, at the call. The catalogue still described the old one — an annotated let, or a field whose declared type says so.user_parallelism = yes for the first time. Found while checking a claim rather than by looking: the analysis records what the constructor makes, but a call handing a hull back was still read as “this analysis did not watch the allocation being made” — the one reason that is never true of a hull’s own constructor. let counter = SharedMut(0) at yes, with nothing crossing a thread, is Rc<Local<i64>> now where it was Arc<Crossing<i64>>; hand it to a spawn and it is atomic in both hulls. That is the per-value answer doing its work on the lock, which is what ADR-057 D3 asked for and what its index row said was missing.spawn lowered that half could not be run at all. It can now, and a program that uses the name after the spawn gets borrow of moved value from the backend about the generated file.user_parallelism exists to serve. Measured before deciding: let counter: SharedMut[i64] = 0 emitted the name untranslated into the language below and came back as cannot find type SharedMut in this scope — Part III C.1’s class, about a name the program did write and the specification does promise. The long spelling Shared[Locked[i64]] made both hulls except where the value was a number, because a literal has no type of its own and the rule that made the hull needed one. So the language’s own counter compiled in neither spelling.update takes a pure function and is therefore repeatable, which is the route to a lock-free implementation by retry — the route Clojure’s atom and Haskell’s TVar actually took. Nothing is promised; this only declines to make the promise impossible.Shared(x), SharedMut(x) and Locked(x) are calls, and the annotation stops being the constructor. The line that settles it settles the nullable case too: a hull you cannot see, the compiler writes; a hull you can see, you write — a T? hides nothing, these three change when a value is cleaned up. What it buys is that the positions stop being a list: there were two, and four holes in them, all the same hole. A number literal could not be given a hull at all while a position had to know one was wanted. A call is an expression, and expressions go everywhere.Shared[Locked[T]] is refused, with the short form in the message. Two spellings of one type were worse than untidy: the same bytes below, two different types above.Shared(db) — a hull you can see is one you write” — where they used to send the reader to another line.lets in the tests, seventeen long spellings, and one test whose premise the change removed (“the idiom is not sync today, because nothing can resolve access” — something can now). One probe had to put the claim back before asking its question, because Ledger::infer runs the sync inference against the shipped ledger and that inference only ever raises.contracts::sharing, contracts::send and three tests. A reader could not tell a plan from a promise on the one sentence the whole Shared design turns on.std’s Nikaia half is lowered once, at the default setting, and linked into programs built at either — sound only while nothing in those files lowers differently per switch. Part III named Shared as what that used to rule out “and no longer does”, because D6 had taken the count off the switch. D2 put the emission back on it, so a Shared in a std .nika file would now be lowered once with the cheap count and handed to a program built at yes. Nothing is broken: tests/sysroot.rs lowers every module at both settings and compares the bytes, so this would be a red build and not a miscompile — and no .nika file in std writes one. What was wrong was the reason printed next to the guard.Shared sits in contracts::send’s container list — answered by what it holds — and the reason used to be “the representation no longer moves with the switch”. It moves again at no, and the row is still right for a better reason: it moved to the setting where nothing crosses. At yes the count is atomic wherever the analysis cannot prove otherwise; at no it is plain everywhere because a task is interleaved on the same thread and D1 closed the way out of the program. Either way the count suffices for what can happen at that setting.NK25xx row, which is about the verdict at both settings and is still true.3000000000 + 1 compiled and 2000000000 + 2000000000 did not. Measured before deciding, and it is worse than an inconsistency: what came back was rustc’s own lint, “this arithmetic operation will overflow”, on the Nikaia line that wrote it — Part III C.1’s class, the one this compiler calls a bug of its own and built NK1116 and NK1118 to close.+. Not where the position gives the number its type: a sequence index and a repeat count are counted by the machine and are deliberately left bare, and a type written into one would pin what the position is there to decide.let a = 2000000000 is an i32, so a + a is an i32 sum and does not fit — refused as NK1116, “this comes to 4000000000, which does not fit in an i32“, with the way out one word away: let a: i64 = …. Widening it would mean widening a’s declaration, a walk back from a use to a binding, which is the analysis this whole line of decisions is cheap for not having. It is also what every language with two integer widths answers.crate::fold; the checker passes what it knows about a name, and the emitter — which has no scope and no types — passes nothing_is_known and thereby gets exactly the subset answerable without looking anything up. Two constant folds in one compiler would be one fold and one liability.i64 holds it, NK1116 where no type does (“does not fit in an i64“) or where a name pinned a narrower one. let small = 2 + 3 and let small = 42 are written exactly as before, so nothing that compiles today stops.nikaia.toml at the repository root was a throwaway project file from a session’s own testing, swept up by a git add -A. The root is a Cargo workspace and not a Nikaia project, and with that file there nikaia run anywhere under the root read it as the project and wrote a nikaia.lock beside it. Five tests across three binaries were failing for that one reason: the backend default, a broken cache, and a directory with no manifest not being a project.extern "C" is a parse error, Target has two values, and this repository has no notion of a linkable artifact. It is written down because it is the first direction that touches user_parallelism at its root, and a target added without it would answer the question by accident.extern "C" brings back is not the representation problem. A C signature names neither Rc nor Arc — it names a pointer and an integer — so ADR-061 D1’s question cannot even be posed there. What it brings back is that the caller owns the threads: an exported entry point may be called twice at once from threads no switch of ours bound. Python’s GIL is not the guarantee it is remembered as — an extension may release it, and free-threaded CPython removes it.user_parallelism bounds your code and cannot answer for a caller’s without meaning two things at once, the second one silently.contracts::send asks nothing about the switch, so a library is already checked for the world it would enter. Only what is emitted moves, and only under the entry points.Shared[T] that reaches a foreign call is an Arc<T>“. Since ADR-061 D1 it does not reach one at all — it is refused, and the way across is what is inside.Shared stays in)Arc<Local<i64>> — an atomic count around a non-atomic lock, a shape that is right under no assumption: either the value can cross and both halves must bear it, or it cannot and neither needs to.Shared may not go into code nothing describes, beside the lock and for the sentence that was already the lock’s reason — a Rust library has one signature. When D6 made the count atomic everywhere there was one representation to name; D7’s per-value inference brought the second back, so a Shared[Conn] is an Rc for one value and an Arc for another in one program. What works today works by accident, and where the accident fails rustc refuses in Rust’s words. The way out is to pass what is inside; a bridge type is reserved and not built, and its name would not be the machine’s.user_parallelism = no every count is plain. Nothing a user writes can cross there, and D1 closed the last way out. The one reason that used to survive — “its callers are in a unit this build cannot see” — does not hold for a Nikaia caller, which is built with this program’s settings and as a crate of this build (ADR-047 D2 rule 4, ADR-053); it held for a Rust one, which D1 now refuses.Shared[Locked[i32]] may go into a task of ours at either setting. D2 is about what is emitted, not what is permitted — the distinction D6 could not make, because then the emission and the verdict were one decision. D3 keeps sharing::infer at the floor for the same reason: what it writes is a contract.no a public signature’s parameter is plain where it was atomic — five of five positions plain; at yes the same program is three plain and two atomic, unchanged. Two cross-file tests now assert that the two files agree rather than that they are atomic, at both settings, because at no they agree the other way round.A menu on every page, generated rather than written down. Five sections — Nikaia, Specification, Decisions, Notes, Examples — as a sidebar where there is room for one and a single Menu line above the text where there is not. scripts/site-prepare.py writes _data/nav.json on each build from the files themselves, and each entry’s label is that page’s own first heading: a page that is retitled retitles itself in the menu, and a decision record that is added appears in it. Sixty-one records are in there today; a list that had to be edited by hand would have been wrong before anyone noticed, which is the whole reason it is generated.
It is <details> and nothing else — no script decides what is open, so the menu works before the page has finished loading and with JavaScript switched off. The section being read ships open and the rest ship closed, which is what keeps a sidebar of sixty-one records readable. Two things script does add, and neither is load-bearing: the menu’s own column scrolls to the page you are on, because the browser will not (on ADR-050 the entry is a long way down), and on a narrow screen the outer disclosure is closed, because there the menu is a control rather than a column. It ships open, so a reader with no script gets a menu rather than a line that will not expand.
Every example program is a page now. examples/*.nika were published as files, and a browser asking for one got a download: no layout, no menu, no highlighting, and the examples index linked at all eleven of them. There is a page beside each of the nineteen programs now — highlighted from the same grammar as everything else — the index links at those pages, and the file itself is still one link away from each. examples/inventory/, which the index has always linked as a directory, has an index instead of a 404.
84 pages and 352 code blocks, up from 66 and 334.
Every code block was double-spaced. Shiki writes <span class="line">…</span> with a real newline between them, and under white-space: pre that newline is what ends the line — so the display: block the stylesheet also gave .line added a second one after every line. It was there from the first build of the themed site and read as though the source had blank lines in it.
/docs/ was a 404 and the note index was published as raw Markdown. jekyll-readme-index follows GitHub’s rule that a docs/README.md is a candidate for the root index, and drops it when a root README.md has already won that slot — so docs/README.md became neither, and was copied out as a .md file. It is given a permalink of /docs/ in the checkout, which is also what the menu’s Notes section needed to point at.
Rc the compiler does not give)user_parallelism = no it compiles to Rc“ has been false since ADR-037 D6 made the atomic count the floor at both settings, with the plain one an optimisation contracts::sharing applies per value where it proves nothing crosses. Measured while checking something else: at no, a value the analysis cannot follow comes out an Arc, exactly as D6 says it should. The page promised the old per-build expansion, which is the shape that record replaced.docs/history/rc-or-arc.md’s quotation of the old sentence, which is evidence in a finding about that very divergence rather than a claim, and the benchmark row labels, where “single-threaded” names a measured case.no was described as a “single-threaded event loop”, which is a statement about the process — and the process has at least two OS threads. The runtime starts named I/O workers (nikaia-io-0, …) whatever the switch says, and its own test asserts “one I/O thread always”. The claim below it — two pieces of your code are never in flight together — was always true, and the word above it was not.no line handed out the one count the switch does not give.std does uses every core whatever the switch says — a file’s text is validated in chunks across all of them, 63.7 ms down to 16.5 ms (ADR-016), with no check of user_parallelism anywhere in that path.wasm32-unknown stops reading as a choice a reader could make: it is named and refused, with the gap stated. The compiler already distinguishes — it carries a has_runtime for exactly this — and only the README flattened it.overlap { … }, and the compiler checks the claim)std grew one per arity.Operation, every pair is asked for a verdict, and a pair that is not Overlap is refused as NK2104 in that verdict’s own words — “both reach stdout, and one writes it”, which is D3’s own example. A branch this compiler cannot account for is not refused: reading an absence as “they meet” would refuse a correct program on nothing (Part III C.4).let inside one would name a thing that leaves by two doors.std has one overlap<n> per arity that polls every branch in one pass in the order it is given, and the emitter hands over the branches that can pause first, read off the ledger’s sync column. Nested binary joins would have made the naive order structural: interleave(a, interleave(b, c)) polls a to completion before b is started, which is the sum D6 names and refuses. The results go back into written order at the call under a comment naming the record, and a block whose branches are all of one kind pays for no permutation.Ok and each element of the tuple takes a ? in written order — and ? returns at the first Err, so “the first in written order wins” is not a comparison this compiler makes. A catch inside a branch handles that branch and is not counted. The error type is written rather than inferred, because an async block with a ? and nothing to infer from is “type annotations needed” about a file nobody wrote.async block and not an async move one, so nothing is moved in and a branch borrows what is around it exactly as an ordinary statement does. That is what makes the form lighter than two spawns.overlap in seven “also possible here” lines, which is the whole of its diff: the word is an expression now.deep directly and on a library depending on the same directory as c was told its deep::Id is not the c::Id that library takes. One type, refused for having two spellings, with a help line asking for what the program had already written.project::renames_in resolves each dependency’s own manifest keys to their directories and gives the one word this build uses for each: the program’s own key where the program names it too, and otherwise the package’s [package] name, which members_of already refuses to let two packages share. Either way it is a function of the directory and not of who asked — whose word wins cannot be “whoever was read first”.Ledger::absorb_renaming), so a name a package declares itself is never renamed: qualify has already put that package’s own word in front of it. A package that keys one of its dependencies with the word a consumer uses for the package itself is the case that makes the order matter, and that key is dropped from the table.modules::collect_with reads is in this program’s ledger to be renamed.str::to_uppercase, str::to_lowercase and their String twins. The owner asked the obvious question — “to_uppercase? I would have expected that to have a type” — and it does: (&str) -> String. It was unwritten, the corpus uses it (line.to_uppercase() in examples/k-nucleotide/src/main.nika), and it is exactly the kind of entry ADR-028 D5 means by “an entry exists because a program asked for it”.std name being absent breaks the day somebody writes it down — and this repository has now done that twice in one session: cli::args().nth(1) stopped being unwritten when Args::nth landed, and String::to_uppercase stopped when this did. typecheck.rs already carried the scar from a third (push_str).contracts.rs’s “a method call is resolved through the receiver” used the same name for its unresolved half. Four times, three files, the same one-line fix found each time by a red test rather than by looking.common::UNDESCRIBED_METHOD — with the reason beside it and what to do when somebody writes that one down. The tests using it say why. a_value_from_a_signature_that_claims_nothing_fits_anywhere rests on something different again: HashMap::keys’s signature says -> ?, which is the ledger declining to claim rather than nobody having got to it, so changing it is open-decisions.md’s question of whether the ledger’s type language grows, and not routine work.fn hand[T](x: T) -> T becomes fn hand(x: T) -> T. The [T] is read by the parser, erased by the checker (ADR-024 D4) and dropped by the emitter, whose function header has no slot for one — so rustc answers “cannot find type T in this scope” about a file nobody wrote, with “you might be missing a type parameter” as the help. Part III C.1’s class.<T> is not the fix, which is worth recording because it is the obvious one. Measured: fn hand<T>(x: T) -> T compiles, and fn shout<T>(x: T) -> String { x.to_uppercase() } gets “no method named to_uppercase found for type parameter T“. The second needs a bound, and the checker cannot ask for one — ADR-024 D4 erases T to ? and ? fits everything, so the body checks clean and fails in rustc. Lowering the parameters turns “every generic fails” into “every generic whose body uses the parameter fails”: the same C.1 class one size smaller. #226 says so with the measurement.? for the fixtures above: D4’s erasure is an absence the language decides, which makes it the right one, and it cannot be used until this is fixed.HashMap::get said a key is always there, and refused the program that asks whether it is)HashMap::get as -> $V where Rust’s hands back an Option<&V>. So counts.get(k)?.n — the honest Nikaia spelling — was refused as NK1121, “reaches through a Tally, which cannot be absent”, while counts.get(k).map_or(0, …) compiled by reaching Rust’s Option surface through the hole. The ledger refused the correct program and accepted the one this language has its own words for, which is Part III C.4’s rule standing on its head.-> $V? now, and both halves follow: counts.get(k)?.n ?? 0 runs, and .map_or on the result is refused as NK1125 with ?. named as the way out — correct, since a method of Option is not a member of a T? in this language.examples/k-nucleotide/src/main.nika is one line of Nikaia rather than one line of Rust: counts.get(fragment)?.n ?? 0. A comment says what it used to be and why.a_value_from_an_unwritten_signature_fits_anywhere used cli::args().nth(1) as the unwritten signature; that entry exists now, so it takes String::to_uppercase instead — and a second test holds the half the old example can no longer show: a signature that is written is measured against the parameter (NK1102).open-decisions.md’s entry on the ledger’s type language, taken. Args::nth as (&Args, at: i64) -> String? (six example programs open with cli::args().nth(1) ?? "…", and the ?? stood on a value of no type), String::push as (&String, c: char), and HashMap::get’s correction above..nika files: 51 → 35 unresolved method calls, and the no-entry bucket is 0 — every call whose receiver type is known now resolves. None of the 35 went: the cascade is untouched, which is what §6 said these entries would and would not buy.? — and §6’s second option, an iterator type for the ledger, is where it lives. The measurement is repeated there..nika files in examples/, tests/samples/ and crates/nikaia-std/src/. 16 are a known receiver whose method no entry describes — Args::nth (10), String::push (5), Tally::map_or (1). The other 35 are a receiver that was already ?: the cascade. Its roots are 24 a local or parameter, 6 a method call whose own result was ?, 4 a field, 1 a free call.HashMap::keys has one, and it says -> ?. So let names = totals.stations.keys().collect() followed by names.sort() is four unanswerable calls from one ?. Of the ledger’s 86 entries: 53 fully written, 9 with a result of ?, 22 with a ? among the parameters, 2 with no signature.keys hands back an iterator, and the ledger’s type language has no word for one: its variables bind from the receiver (ADR-031), so -> $K is sayable and “a sequence of $K, lazily” is not. collect is the same gap from the other side — what it builds depends on the target, which no receiver-bound signature can name.open-decisions.md’s entry on whether the ledger’s type language grows, with the three options and what each closes. Filling what is sayable today closes 16 of the 51 and none of the cascade; an iterator type is where the remaining two thirds are.Some(…) only where it knows the value is not already a T? — right, because wrapping one that is makes an Option<Option<T>> — and stayed silent otherwise, so the program failed in the language below with rustc’s “try wrapping the expression in Some” about a form Nikaia does not have. value.into() is correct in both directions: Rust has From<T> for Option<T> and the blanket identity From<T> for T. So the uncertainty the rule was conservative about is made irrelevant rather than guessed at..clone() and strip_prefix, neither in any ledger — where one is a plain String and the other is already an Option<&str>. The same emitted .into() wraps the first and passes the second through: Ada, da, no prefix. Neither method compiled before, and Some(…) would have been wrong for the second.Some(…) stays where the type is known (D2), in an impl and in a free function alike, because it says what the source means where a conversion only does what it means — ADR-004 D2’s pressure, paid only where the compiler could not work the type out and honest about that.Constructor, Conversion, or nothing for a value that needs neither. The copies had already drifted in what they did with an unknown type, which is how a rule stated five times goes wrong.return in an impl method does not get its Some(…)” named the position, and the position is not it — nullable_sites holds two of four returns, one in an impl and one free, and what separates them is whether the ledger describes the value. Growing the ledger until clone is in it is worth doing and was not the fix: the hole is “a type this checker could not work out”, and the next program reaches it under another name.T? is refused where it is written, not where it runs)NK1125, and the correction that produced it was mine to make. This was first written down as an open question — whether a?.b.c should mean a?.b?.c — on the claim that “a ?. short-circuits the rest of its chain in every language that has one”. That claim conflates two things and the owner said so. The short-circuit decides whether what follows is evaluated when the guard fails; it never makes what follows safe. a?.b.c guards a and nothing else: absent a gives null and .c is never reached, while a present a with an absent a.b reaches .c with nothing to reach on.TypeError, a NullReferenceException — because null inhabits every reference type. This language has no such value. Types are non-nullable by default and T? is a separate type (Part I 2.3), so .c on one is a member the type does not have: the same kind of mistake as .c on an i64, answerable where it is written.find(1)?.b.c and a bare maybe().c — no ?. anywhere — were equally unchecked, and both came out as a field read off an Option for rustc to refuse about a file nobody wrote.NK1121’s mirror, and says so: that one refuses a ?. where there is nothing to reach through, this one refuses a plain . where there is. The way out is the guarded form — write \?.c` — or ?? and then the plain .`, and a test runs both. Nothing in the corpus was refused by it. ADR-066 D6.?. reaches a method, and ?? chains)?.m() with a sentence, on the reading that the section’s example writes a field; that record’s §4 wrote down what the refusal rested on — “whether the language should have it is the owner’s question and not this record’s” — and the owner answered it. So this is a reading settled, not a form added: the section needs no new sentence, only a status note that stops saying no.; zone, a trailing lambda. A ?. decides whether the call happens and never what a call is, so what it may throw, whether it pauses, what its arguments have to be and what the receiver’s type binds are all unchanged — and one function in the checker answers both spellings, so there are not two copies of those rules to drift apart.match and not to the field’s map, which is the one place the two members differ. A field access can neither pause nor fail, so a closure is somewhere it can happen; a method call may do both, and inside a closure an .await does not compile and a ? has nowhere to go. map would have bought a ?.m() that works for the easy half of the language and refuses the rest. The test that holds it reaches a method which is both fallible and pausing.T?, by ADR-052 D6’s rule and recorded in the same set — otherwise a?.b()?.c would reach through a nullable of a nullable.?? chains. a ?? b ?? c was a parse error naming the second ??, in a language whose page says the operator provides a fallback and nowhere says a value may have only one: the tail was parsed one level below the rule itself, so it could not hold another. Right-associative, which is what the types ask for — the last fallback is the plain value that ends the chain. Not a decision that was made and lost; a precedence accident.NK1121 keeps the spelling the reader wrote: the way out of a reach through a plain value is now .name(…) where a call was written, and .name where a field was. A reader told to drop one character should be told which.Expr::SafeMethod is its own AST variant and not a flag, for ADR-052 D2’s reason — every analysis has to say what it does with a call that may not happen, and a flag is the shape that lets one forget. Nine were asked and answered. tests/nullable.rs runs four programs rather than comparing text, because short-circuiting is not a question a type check settles: a reach that went through a null and one that did not both compile.Some(…) and no word — fixed below.T? with a plain . was emitted rather than refused — fixed in the same session, below.yes half gave a task a thread — and the program neither half could run now runs: four tasks bumping one SharedMut[i64] 2 500 times each print 10 000, and the same source prints the same number at no.yes, where the Send bound is, and the one-thread queue at no, which has none.Send gap turns out to be unreachable rather than narrow, which is worth writing down because it changes why it should be built. The two shapes that could carry a non-Send task future are chosen per value by an inference that already sees the crossing: a Shared[Note] bound inside a task body and held across a file read lowers to std::sync::Arc, and a SharedMut[i64] to Arc<lock::Crossing<…>>. Both were compiled and run to check it. So no program this compiler can lower produces one — which is the reason to add the refusal now, since it costs nothing before there is anything it would reject.clippy::useless_format on NK1123’s note. The gates are all six or none.user_parallelism = yes)no and 0.65 s at yes on four cores. Part II 12.2’s counter — the program the switch exists to serve — runs on more than one core now, and nothing in the former backlog file’s order list waits on a thread any more.rayon’s pool is a work-stealing pool for closures: it takes a FnOnce and runs it to the end, and there is no point at which a closure gives its thread up. A task can pause, so what a pool would have to hold for one is a future to be polled again later. rt::pool is a queue of futures over the same worker count (user-pool); rayon keeps fs::map’s UTF-8 check and the parallel piece driver, which is work it is good at.Send (§2 D6); a waker has to reach the queue from any thread, which makes a task an Arc<Cell> a waker pushes back rather than a flag one loop reads; and exactly one thread at a time may wait in the I/O, because the ring is behind a lock and a thread inside io_uring_enter is woken by the kernel and by nothing else. That thread is the pilot, and the others wait on the pool’s bell.main does not take the I/O park either, while the pool is carrying tasks. It waits on the runtime’s bell, and everything that could wake it rings that — a completion the pilot noticed, a Slot filled by a task, a task finishing. Without it a .join() across a thread would be a main asleep in the kernel that no pool thread could reach, which on the completion path is a hang rather than a delay.TaskHandle::start_on_pool at yes and TaskHandle::start at no. Two functions and not one with a bound, because the bound belongs to the build: a program at the default is never asked for a Send its setting does not need, which is what keeps ADR-061 D1’s plain count reachable from inside a task. ADR-006 D5’s drain asks both queues, so “a task nobody joins still runs” is as true on the pool as on the thread.Send as a refusal of ours. The pool’s starter asks for it, so a task holding something that may not cross is refused — by rustc, about the generated file, which Part III C.1 calls a bug in this compiler. #254 carries it with what it needs: ADR-005 §1 Group B’s structural check already runs on what a task captures, and what it has never been asked about is what a body holds across a pause.rustc wrapper, which Cargo runs only for a package it already thinks is stale. Flipping user-parallelism in nikaia.toml therefore left the package fresh and the program running at the setting it was built at. The cache had the switch as a dimension (ADR-021 D5) and nothing ever got as far as asking it.rustc emits names the generated Rust, so an edited .nika left the package fresh. Both were fixed by giving Cargo the right file. So is this one: the driver writes the resolved switches to target/nikaia/gen/switches before cargo runs — only when they differ, so an unchanged switch dirties nothing — and the wrapper puts that file into the dependency file beside the .nika sources. A switch that changed is an input that changed.yes executor, which is the shape of it: the switch decided nothing visible until it decided which executor a task ran on. The test flips it four times, because the first flip passed before the fix — Cargo had something else to rebuild — and only going back caught it.examples/, zero of which overlapped. What a reader was asked to hold was a rule about statement order that no program in this repository ever used, and that is a bad trade at any price. overlap { … } is what replaces it, and D1’s own argument is that a programmer saying where an overlap may happen is worth more than a compiler guessing.seq { … } and the ordering switch go with it (D7), because both exist only to turn the inference off. seq has left the reserved words — 30 down to 29, so a program may call a variable seq again — and ordering is no longer a manifest key, a CLI flag, or a ADR-021 D5 build-cache dimension. A switch that selects between two behaviours when there is only one left is a question with no answer.touches is still a ledger column, the resource kinds are still the vocabulary, and verdict still answers why two operations may not run together — but it now checks a claim the program made instead of making one of its own, which is NK2104. ADR-033 D9’s refusals outlived the feature they were written for, word for word, and --overlaps still prints them: about the blocks a program wrote rather than about every adjacent pair.std too, since nothing can reach them: task::both (the pool’s closure pair), task::interleave (its twin for halves that pause, now task::overlap2) and task::read_pair (D10’s free pair of reads). fs::read_both stays — it is the pair a program asks for, and it always overlaps because somebody wrote it. benches/overlap --bin runtime calls rayon::join directly so ADR-033 §8.4’s number, the ~46 µs a closure pair costs, stays reproducible after the vehicle that used it went: that number is why D2’s vehicle takes futures.ordering now fails, which it did not: the key stayed on the accepted list after the setting behind it went, so a build read it, ignored it and said nothing — a program differing from the file that describes it. It is refused in its own words rather than as a misspelling, because whoever wrote it meant it, and the refusal names overlap { … }. The distinction is why there are two lists: a moved key (cleanup-deadline) still decides something elsewhere, so the manifest keeps working and says where to look; a withdrawn one decides nothing anywhere.order::Vehicle, in_flight_read and Carries, and the emitter’s statement grouping. ADR-033 stops being provisional by being displaced rather than by being measured, and its status line says so.This one was introduced and fixed inside this release, and the two entries it had are one: the drain that makes a task nobody joins still run is what made a task that never finishes hold the program, and the bound is what it had been missing.
main’s value until the started queue empties is what makes ADR-055 D5’s “a task nobody joins still runs” true, and it meant a task that never completes never let the program exit. ADR-006 D5 already covered this — “waits for parked cleanups and detached tasks, bounded by cleanup-deadline” — so nothing here is a new rule, only the half of an old one that had never been built.main’s value arrives and not before, because what the deadline bounds is the wait for the tasks and main itself may run for as long as it likes. "0" disables draining, which is that decision’s own word for it. On expiry the program says how many tasks it abandoned and what the bound was, rather than exiting quietly.overlap met rather than made)let takes one name, and the specification writes it taking several. Part I 8.1.2’s own example is let (user, rights, prefs) = overlap { … } and Part II 12.5 writes let (tx, rx) = channel::bounded(100) — a form the specification uses twice, for two different constructs, and defines nowhere. An overlap is reached by its tuple in the meantime, which works and reads worse than what the page promises. In #224 with both sites as its evidence.let big = 3000000000 compiles)i32” — about a type the program never wrote. Nothing about it is wrong: the number is a number and the program has said nothing about its size.L and Rust refuses. This language was in the smaller camp by inheritance rather than by choice.let small = 42 followed by a call taking an i64 compiles today and keeps compiling. Where nothing asks, the literal takes the first type that holds it: i32, else i64. Kotlin taken whole would have cost the first half, and Part I 2.4’s second example is exactly that program.i32 cannot hold has no second answer a use could ask for — an integer literal is never an f64, u8 is narrower, and there is no wider integer — so the rule is: fits an i32, change nothing; does not, emit an i64.emit::integer_literal writes a literal an i32 does not hold as an i64 and every other one exactly as before, so let big = 3000000000 compiles and prints while let small = 42 handed to a parameter taking an i64 keeps compiling — the literal that fits carries no type of its own, so the use still decides. No use-site walk, no second inference, nothing else touched.Unary arm rather than inside that rule: -2147483648 arrives as a negation of 2147483648, one past i32::MAX, and a rule reading the literal alone would widen the one number where widening is wrong. One further out, -2147483649, is an i64 and keeps its sign. Three tests in tests/literals.rs.3000000000 + 1 compiles, because the first operand widened; 2000000000 + 2000000000 is still refused, because neither operand is out of range and the sum is not a literal. That the two lines differ is not predictable from Part I 2.4, so it was a question with what either direction costs, since answered by ADR-063 — D3’s “no analysis” is exactly what folding a sum in the emitter would spend. Part I 2.2’s note says which of the two is which.access reads, update writes, and no lambda is handed something mutable)access’s block mutated through its parameter, and a lambda’s parameter has no spelling that says it may be — so the choice looked like “add a spelling” against “let mutability arrive invisibly from the callee’s contract”. Two facts, looked up: the rule that a reader sees mut is unbroken across every program in this repository, and the grammar’s one place where a callee may change what it was handed is spelled — &mut self.access exists because “a list of ten thousand entries is not copied to append one”. let mut v = old inside update lowers to let mut v = old; — a move, three words, whatever the size. update could already append without copying, with the mutation spelled by the word this language has, in the place it has it.access reads in place and may not change what it is handed (D1) — the one thing neither other door does, since get() copies and update must return the new state. update is where locked data changes (D2), and old is an ordinary immutable parameter. Therefore no lambda is handed something it may change, and there is nothing to spell (D3) — Part I 5.3’s one lambda form untouched, and the reversible direction taken: a spelling can still be added later, where shipping invisible mutability and requiring a word for it afterwards would have invalidated every lambda already written.Option so update moves it out and back without a Default bound or unsafe code, and a lambda that fails leaves the lock empty — reported as that rather than read as something else. std.contracts describes all four.access moves sides in the ordering rule: it is a read now, so two accesses of one lock are unordered like two gets, and the writing forms are set and update.access_all reads and update writes one, so Chapter 12’s own transfer between two accounts — take from one, give to the other, under both locks — has no door. Part II 12.2 carries a status note saying so and #245 carries the entry; what such a door is called is a decision and not a footnote.access through on the cheap shape while the crossing shape’s owner check refused it, which is two behaviours for one written type. Reading is exclusive too, so re-entering a lock is a defect through whichever door it happens.open-decisions.md lists what is open, and nothing else)std::http question a parallel session put while this was being rewritten — and the answers are where they belong.use lines, and that nesting the modules contradicts ADR-047 D2 rule 3. Both are in that record now, with what each costs. Everything else was already in ADR-046 through ADR-055.docs/error-corpus.md cited open-decisions.md §5, §7 or §9 for answers that ADR-050, ADR-051 and ADR-055 hold. A notes-page section number is the worst possible anchor for a decision — it renumbers whenever an entry closes, which is what this change does to all nine.access hands its lambda (the surface of Locked[T], which ADR-057 left undecided and Part II 12.2’s own example needs); whether an un-annotated integer literal has a type (the last NK1116-class defect, which cannot be widened without refusing a correct program); whether the language has an unconditional loop (nothing is half-built, which is why it is a decision and not work); what a program may do at compile time (ADR-026 is Open and holds the six questions — the entry names Q4 as the one blocking the others rather than restating them); and how a package is named by a version.std::http blocks no work at all and belongs anyway: it blocks reading an accepted record whose surface half cannot be evaluated, which is a cost that grows silently. What does not belong is a question nothing rests on — which SQLite binding std::db would use is undecided and blocks nothing, because std::db does not exist in any form. That is scope, the roadmap holds it, and scope becomes a decision when something comes to rest on it.async zero times and at user_parallelism = no the blocked thread is the program. Every sentence in that section was a promise about a lowering nobody had written. It now says what is built, what still is not, and that “you do not write a keyword” is checked by a test that reads the corpus rather than promised.no column is built; its yes column — a work-stealing scheduler — is not, and the note says why rayon is not it.@detached note said spawn does not lower and the capture it decides is not reported. Both are built. The replacement also states the narrowness of the move rule, which is the part worth knowing: only a type that is known and that a move takes away, so a number, a char and a view are not it, a Shared[T] handle is exempt, and an assignment in between clears it.sync changed job. It was a claim the compiler checked and nothing acted on; it now decides whether a function is compiled as async fn or plain fn, which makes ADR-027’s conservatism load-bearing and a regression in the inference a regression in the output. Measured: 22 sync against 17 that can pause, across examples/.task::scope note says what it would now be built on, since the executor that owns tasks and the spawn that feeds it both exist — so what a scope adds is the waiting and the touch propagation, not a mechanism.overlap note and Part III 13.3’s user_parallelism note no longer say spawn is unbuilt.async, has an executor, suspends at a file read, or can spawn. Phase 0 now has both records, with what each has built, what building it corrected, what the record claimed and building it disproved, and what is open — and the ✅ status list has the line it was missing.overlap, and the rest of the former backlog file’s order can be taken as it stands.open-decisions.md was missing the decision this session actually asked for. The question “what does a task mean at user_parallelism = no?” is exactly what that page is for — it blocked the largest item in #219, it could not be answered from the code, and the answer changed what the compiler emits. It is §9 now, with why it had to be asked, the three readings a synchronous lowering allowed and what each broke, and what the answer cost. §5’s heading said overlap waits on the runtime binding spawn waits on; the binding is built, so it says what overlap can lean on instead.NK2101 is raised now), Part I 8.1.1’s overlap note and Part III 13.3’s user_parallelism note (both said spawn is not built), and docs/spec-promises.md’s diagnostic lists — those last are left as they were, with a note saying so, because that page’s whole value is being readable against the day it was written.task::both is named as the pool’s vehicle where the roadmap and ADR-033 describe it, because it is one of two now: D10’s question is unchanged and the answer has a second arm.
Locked[T] has a shape, and it is decided per value)user_parallelism = no, an OS mutex at yes — which puts a value’s ability to cross a thread in the hands of whoever builds the program. The investigated alternative, a mutex everywhere, charges 11.3 ns an acquisition at no, which is exactly what that switch exists to save. The third answer was already in the building: the reference count asks this question per value and answers it.Rc and Arc differ in cost and nothing else, a borrow flag and a bare mutex differ in behaviour. The mutex shape carries an owner check for it, measured at +2.0 ns, ×1.15 (docs/mutex-floor.md §4.3).Locked::new in the language any more than there is a Shared::new; Locked::access is in std.contracts with sync = "from(f)", which is the entry docs/mutex-floor.md §3.1 found missing and what makes Part II 12.2’s own idiom sync; and both shapes translate back to Locked in a relayed message like every other substituted name.access hands its lambda (Part II 12.2’s counter.access fn(n) { n + 1 } does not compile as written — a question about the surface), and the analysis watching a Shared[Locked[T]] allocation, without which the cheap shape never triggers at yes.crates/nikaia/tests/lock_floor.rs stays a probe of both answers: it now cuts the shipped Locked::access entry out before putting a variant in, so the decision keeps its evidence instead of only its conclusion.Locked[T] has a shape, and it is decided per value)user_parallelism = no, an OS mutex at yes — which puts a value’s ability to cross a thread in the hands of whoever builds the program. The investigated alternative, a mutex everywhere, charges 11.3 ns an acquisition at no, which is exactly what that switch exists to save. The third answer was already in the building: the reference count asks this question per value and answers it.Rc and Arc differ in cost and nothing else, a borrow flag and a bare mutex differ in behaviour. The mutex shape carries an owner check for it, measured at +2.0 ns, ×1.15 (docs/mutex-floor.md §4.3).Locked::new in the language any more than there is a Shared::new; Locked::access is in std.contracts with sync = "from(f)", which is the entry docs/mutex-floor.md §3.1 found missing and what makes Part II 12.2’s own idiom sync; and both shapes translate back to Locked in a relayed message like every other substituted name.access hands its lambda (Part II 12.2’s counter.access fn(n) { n + 1 } does not compile as written — a question about the surface), and the analysis watching a Shared[Locked[T]] allocation, without which the cheap shape never triggers at yes.crates/nikaia/tests/lock_floor.rs stays a probe of both answers: it now cuts the shipped Locked::access entry out before putting a variant in, so the decision keeps its evidence instead of only its conclusion.spawn lowers, and a task interleaves on the one thread)spawn fn { … } becomes a future the executor owns, .join() is an .await read off the ledger like any other pausing call, and both mean the same thing at either setting of user_parallelism, which is what Part II 11.2 calls the uniform API. Two tasks reading two files are both in flight before either finishes, on one thread — that section’s own sentence, now about a program somebody writes rather than about std’s own futures.async block and never a closure, for the reason step 3 gave: a task’s body may pause, and Rust has no stable async closure. move is Part I 8.3’s implicit move and Rust’s move meeting at the same place, so the generated program has no move keyword the source did not imply.spawn took ( expr ), which Part I 8.2 called “a bug in the parser and not a second form”. It takes the trailing lambda of 5.3 now, so there is one lambda form in the language. The parenthesised spelling stays as a sentence rather than as an alternative, because programs were written against it — and a lambda that names an argument is refused as NK2103: a task is handed nothing, and dropping the name silently would leave the body naming something nothing declares.block_on returned the moment main was ready, so a spawned task was a future in a queue nobody polls again — and D5 says it runs, which Part I 8.2’s own example needs, since it keeps no handle. main’s value is held now until the queue empties..await was a spelling the specification offered on a handle, beside .join(), while Part I 8.1 says a Nikaia program contains the word nowhere and a test reads the corpus to hold it. The two contradicted each other and D5 settles it: .join() is where two tasks meet again, which is worth a word, and the .await is the compiler’s to write.NK2101: data a task took with it, used again afterwards)rustc’s “borrow of moved value: message“, whose advice was “consider cloning the value before moving it into the closure” — a closure the program does not have, about a file nobody wrote (Part III C.1). The message names the value, says a task may outlive the function that started it, and gives Part I 8.3’s own way out.bool, a char and a view are copied, which is why let message = "Hello" is not this case and "Hello".to_string() is; a handle on a Shared[T] is duplicated, which is the exemption Part I 8.3 states (ADR-040 D1, D5); and a type nothing describes is not claimed about at all (C.4).message = "other" gives the name a value again, Rust accepts it, and refusing it would refuse a correct program — so the analysis collects reads and writes separately rather than treating every mention as a use.spawn now hands back a TaskHandle[T] of what the task’s body came to, which is why handle.join() has a type at all. A task’s body is a block, and a block’s value is something the checker already knows — where what a lambda hands back is written down nowhere (ADR-029 D1), so nothing is inferred that a signature did not say.std suspends for real, and the executor is the only place a program parks)rt::exec::block_on waits in the I/O when nothing else on the thread can move. Before this, ADR-038 D3’s operations blocked the calling thread — the ring waited in io_uring_enter, the fallback on a reply channel — so a suspension point is now a place the executor runs something else, which is what step 1 built the queue for.runtime.rs already forced each way.async is the easy half, and what a body does is a mechanism.rayon::join, which takes closures, and Rust has no stable async closure — so task::interleave takes two futures instead, an async block is one, and what it does at user_parallelism = no is Part II 11.2’s own sentence: both polled, whichever suspends gives the thread to the other. No thread wake-up at all, where the pool’s costs ~48 µs. The choice is per group and not per function, because a function may be async for a call that has nothing to do with the group — and task::both is still what a pair that cannot pause gets, which ordering.rs has one of: two cli::args() reads.cli::args was never a pausing entry and the ledger said it was. An absent sync line means not sync — std.contracts’s own conservatism — and reading the vector the process was started with is memory. Nobody had classified it, and awaiting std’s pausing entries is what surfaced it: cli::args() became cli::args().await and Args is not a future.std::io is async and does not suspend yet, and the module says so rather than leaving it to be discovered. That is D3’s own split: its mechanism serves files, and a stream needs the readiness half, which is built for sockets and not wired to standard input. An async fn that never awaits finishes on its first poll, so a caller sees what it saw before. fs::map is the same shape for a different reason — a mapping is an open, a stat and an mmap, with no transfer to complete.runtime benchmark’s sequential shape is driven by block_on now, because that is what the emitted program is; its pooled shape reaches the runtime’s synchronous surface, because a closure on a pool cannot await.rt::uring’s rule 3 read “finished and nobody in a call for it” as free. That was true while every caller finished its operation inside the call that started it. A future does not: it is started at its first poll and its answer is taken at a later one, and in between the slot is finished and nobody is in a call for it — so the buffer the kernel had already filled was dropped, and the next operation reused the slot. A read came back as another operation’s bytes.ordering.rs’s a_group_of_four_compiles_and_runs printed ccc leer where the program says zwei vier — a read that returned the data a write in the same group had been given.Drop gives it back, so a future dropped before it finished — a catch that diverted, a task nobody polled again — still returns its slot rather than leaking it. The buffer is not freed there, which is the soundness rule rather than an omission.eight_reads_at_once_each_get_their_own_bytes is the shape that finds it: every file holds different contents, all eight slots are held at once, and each answer is checked against the file it was asked for.async fn where the ledger says a function can pause)sync column says can pause is emitted as an async fn; every call to one carries an .await. Nothing in the .nika source says either word, and nobody wrote sync either — ADR-027 D1 infers it per function over the call graph.sync and 17 can pause. Making everything async would have been the same diff and a lie about what a call costs; §5’s own objection to this decision was exactly that, and the numbers are the answer to it..await goes before the ?, and the order is not a choice: the future is what can fail, so it has to be driven before there is a Result to propagate. f().await?, never f()?.await — the second does not compile.main is driven rather than called. ADR-038 D4 already made fn main the runtime’s; where the program’s entry point can pause it is now a future, and rt::exec::block_on — the executor step 1 built — is what drives it. A main that cannot pause is still called directly, so a program with no I/O pays for no executor.examples/json/src/main.nika is where that arrived rather than being reasoned about: its show and longest are both recursive and both pausing, so the corpus had async Rust’s first sharp edge on the day the step landed. rustc said “recursion in an async fn requires boxing” about the generated file, which is the Part III C.1 class.countdown boxes the call it makes to itself; the call main makes to it holds that future exactly once, is finite, and pays no allocation. The first version boxed every call to anything recursive and charged every caller for its callee’s shape. A -> A beside B -> B where B also calls A is why “both are in a cycle” is the wrong test: nothing about B -> A is recursive, so the test is that the callee can reach the caller again..await from the checker, the way it already gets its ?: stats.add(5) names add, and only a type checker knows what it goes to (ADR-028). Checked::pausing_methods is fallible_methods one ledger column over, narrowed the same way — a name that in one statement is both a pausing call and a call that is not is in neither set, because an .await on something that is not a future is the same kind of message about the same file nobody wrote.std is deliberately not consulted yet. A pausing std entry still blocks its thread until step 3, so awaiting one would be awaiting a value rather than a future. That is what makes step 2 compilable on its own instead of having to land with step 3.async closure, so there is nothing to write — and the refusal is deliberately not a type error, because examples/fortunes.nika’s route handler is a lambda that queries a database and is a correct program. Refusing it in the checker would refuse a correct program, which is the one thing the compiler may never do (Part III C.4); so it still checks clean, and only a build meets the limit. Found by the corpus: the earlier measurement said no corpus lambda called a pausing function, and fortunes.nika is one.examples/inventory stopped compiling the moment its render became async: “implicit elided lifetime not allowed here” about Vec<Entry>. Entry is declared in stock.nika and named in page.nika, each file is emitted from its own parse, and the set of view-holding structs was per file — so the lifetime went unwritten in the file that did not declare the struct. Rust’s elision covered it in a plain fn and stops covering it in an async fn.impl header too: an impl and the struct it is for may sit in different files of one package.TrustedMap for a map whose input the operator chose, Rc or Arc for a Shared[T], and in time a DSL’s shadow struct and a grammar’s generated types. Two of them had been decided one at a time and in opposite directions; a third would have arrived with nothing noticing. Every substituted name is put back, including nested ones: Arc<Vec<Rc<Conn>>> reads as Shared[Vec<Shared[Conn]>].Shared[T] was the one deliberately left in Rust’s words, on the ground that Rc and Arc are different types and “expected Shared[T], found Shared[T]” would hide a defect in this compiler. That weighed two options where there are three: a reader who wrote Shared[Conn] is no better served by Rc and Arc, and Part III C.1 calls an untranslated backend error reaching them a bug in this compiler — and names the third way in its next sentence.expected … found … is not a fact about the program: it means this compiler emitted two different Rust types for one of the program’s, which is a defect of its own. It is shown as “internal error: this is a Nikaia bug, please report it”, with what the backend actually said — displayed, and appended to a log beside the generated Rust whose path the build names. The reader is told their program may well be fine.rustc itself wrote that way — two types of one name, from two crates — is a real thing to say about a real program and stays one. The rule is not “names one thing twice” but “only became that way here”, so a substitution added later needs no entry anywhere.Shared[T] stays in the backend’s words now asserts the internal error.no half: rt::exec. A task queue, a Waker written out by hand over an atomic flag, block_on, a Slot a task’s value leaves through, and Yield — the smallest suspension point there is.two_tasks_interleave_on_one_thread, and it is Part II 11.2’s own sentence: two tasks that yield between their halves come out a1 b1 a2 b2. That was unreachable with the synchronous lowering, because neither closure had a point at which it gave the thread up. A suspension point is a Poll::Pending now, and the executor runs something else.Slot wakes whoever is waiting on it, which is what will make .join() a suspension point rather than a wait.Waker is four functions rather than a dependency. std has no constructor for one without the unstable Wake trait, and each of the four accounts for exactly one Arc; the flag is a bit rather than a queue of wakers, so a wake that arrives while the task is already queued is free rather than a duplicate.block_on panics rather than parks where a task is waiting and nothing can wake it. Until step 3 a pausing std entry blocks the thread instead of returning a future, so that state means a future returned Pending without arranging a wake — a defect in std, and a hang is the worst way to report one.yes half is not built, and the reason is worth having: rayon’s pool is a work-stealing pool for closures, not an executor for futures, so the multi-threaded half is a second executor over the same worker count rather than a use of that one — and it is the step where a spawned future has to be Send (§2 D6).async where a function can pause)spawn waits on turned out to need a decision first, and this is it. The question is what a task means at user_parallelism = no: Part II 11.2 says “interleaved on the same thread”, and two synchronous Rust closures cannot interleave because neither yields. Measured: the emitted Rust contains the word async zero times, and a pause is a thread that blocks.spawn (deadlocks at one setting where the other runs, and makes Part II 11.2 false), run it at the .join() (silently drops a task nobody joins, and Part I 8.2’s own example keeps no handle), or refuse spawn at no (makes a library un-compilable where it is consumed). The owner chose the fourth: the lowering moves.async fn’s”; ADR-006 exists because “any function may pause at I/O”. The language has been specified as implicitly async since Stage 0 began; only the lowering never was.sync — which is “cannot pause”, the question async fn asks. And its conservatism is already the safe direction (D2): an unresolved call costs a function its claim, which here makes it async, and an async fn that never awaits finishes on its first poll while a plain fn that needed to pause does not compile. The error the inference can make is the cheap one..await is punctuation, not resolution. ADR-023 D8 already makes a call to a throws function carry a ? where the ledger says the callee can fail; this reads the neighbouring column. So ADR-011 D2’s “name for name, and the emitter resolves nothing” survives — the same consultation, one column over.tokio would put two event loops in one process, which is the hazard D7 treats when a foreign crate causes it. What is missing is the task queue and the waker — the half rt does not have.user_parallelism then decides only how many threads the executor has, which is what makes Part II 11.2’s sentence true: at no a task really does interleave on the one thread, because a suspension point is a place the executor may run something else rather than a thread that blocks. Part I 1.2 holds — the set of schedules narrows and a program’s meaning does not change.Send at yes.spawn entry is now step 4 of five rather than the first thing to do.
as could name any type the language below has)n as u128 compiled and ran, and so did n as isize — a type ADR-048 D2 deliberately left out. The type list had been narrowed; what an as may name never was, because nothing had decided it. Refused now as NK1122, naming the type: the eight of Part I 2.2 and nothing else (ADR-054 D1).-3 as usize was 18,446,744,073,709,551,613, silently — in a language where a conversion that does not fit aborts (ADR-043 D4), and where the same conversion at an index has reported as an access out of bounds since ADR-048 D1. Written by the compiler it reported; written by hand it reported nothing.std function whose Rust parameter is a usize still needs a written conversion”, and that question is real — but " ".repeat(indent as usize) was not the edge of a deferred scope, it was the one place a program fell into the hole above.i64, and the compiler writes the conversion)repeat’s parameter as n: ? — the absence of a claim — and carried no usize anywhere, so there was nothing for a rule to apply to. Writing it down is what made the answer obvious.std.contracts says str::repeat(&str, n: i64) -> String, and the emitter writes nikaia_std::count::of(…) around the argument. A program writes " ".repeat(indent) and no conversion in either direction.index::at reports a negative as an access out of bounds because it is one; a negative count is not an access at all, so count::of has a message of its own — “a count cannot be negative: the count is -1”. Reusing the index message would name something the program does not do.len: what it encodes is a fact about Rust’s library, not about this language, and the emitter does not know types (ADR-011 D2). The list is one entry long because the ledger has one such parameter; a_count_parameter_is_an_i64_and_the_emitter_knows_its_name fails when the two drift apart. Safe as a name because count::of is the identity for anything that is not a number, so a method of the program’s own called repeat passes through untouched.of(3) has nothing to infer its argument type from, and cannot infer type about a generated file is what Part III C.1 forbids.examples/json/src/main.nika loses both casts and its indent becomes an i64 — which is the answer arriving from the direction §4 asked it to, since with the parameter written down the type checker could finally say the i32 did not match.NK1117 reaches a name inside an expression. It used to fire only where a statement was one name, so let n = q + 1 was passed over in silence and rustc refused the generated file about a name the user did write — the Part III C.1 class. The #266 entry said this needed a decision because “the list of what counts as declaring a name has to be complete before the rule can be widened, and today it is not”. That was right, and the corpus is what said which two sources were missing.<for> declares a name — <for r in :rows>{r.name}</for> (Part II 10.6, ADR-017) — and the hole walk did not know it, because template_holes flattened a <for> and dropped its binding. examples/escaping/src/main.nika is what found it: widening the rule refused its {r.shade} until the walk carried a scope.examples/tally/src/main.nika’s f"{lines}{separator}{blank}" names one. That is a defect the widening found rather than caused.a/b/c message applies, the general one stands aside, because the specific message says what happened to the form and the general one only that a name is unknown.quote { … } was the last row of docs/spec-promises.md marked “means something else”, and it is a refusal now. let q = quote { 1 + 1 } parsed as let q = quote and a block, and lowered in silence. The construct (Part II 10.3) is still unbuilt; it no longer means something else without saying so.let n = 1_000 is caught in an expression too, which is where the misparse the statement rule was built for actually appears.§1.1 in three records, the specification and five tests silently came to mean something else. Every one of them now names the entry’s subject instead, and the page says to do that: what an entry is stays put, where it sits does not.nikaia run printed a warning twice — once translated against the .nika line, and once as rustc about target/nikaia/gen/….rs, which is what Part III C.1 forbids. The cause was the second Cargo invocation: a run cannot use the JSON channel, because the program’s own output is on that stdout, so Cargo rendered its cached diagnostics to stderr while checking freshness and nothing intercepted them.CARGO_TARGET_DIR; a second place working those rules out would have to keep agreeing with Cargo forever. The build’s own JSON says it outright, on the compiler-artifact line whose executable is not null — output this compiler already captures in order to read the diagnostics. Where no line says, cargo run is still what happens, so the change can only remove a duplicate and never lose a run.1 rather than success.nikaia build on Part I 2.3’s own example prints “warning: src/main.nika:2:5: value assigned to maybe is never read” with the caret on the right statement — the translation path handles a warning exactly as it handles an error, and is_about_the_program already drops one that maps to no .nika line.nikaia run, which prints the warning twice: the translated one, and then rustc’s own spanned against target/nikaia/gen/….rs. A run cannot use --message-format=json — the program’s own output is on that stdout — so Cargo renders its cached diagnostics to stderr while checking freshness, and nothing intercepts them.--quiet was tried and reverted. It removes Cargo’s progress lines and not the diagnostic replay, so it changes what a reader sees without fixing anything; shipping it under a fix that did not work would have been worse than leaving the defect named.fn name() -> &str { "Ada" } compiles. It emitted fn name() -> &str and rustc refused it — “missing lifetime specifier: this function’s return type contains a borrowed value, but there is no value for it to be borrowed from” — about a file nobody wrote (Part III C.1).'static is the only lifetime that can be written there, which is what makes this a derivation rather than a choice. A view’s lifetime comes from the input and Rust’s elision takes it from a reference among the arguments; a function with no reference among them and no receiver has nothing to take it from, so a view it hands back can only point at something outliving the program.rustc still checks the body: a body that cannot honour the 'static is refused about the body — measured, fn first(xs: Vec[String]) -> &str comes back as “cannot return value referencing function parameter xs“, on the Nikaia line, which is the message a reader wants. And it never accepts a wrong one, since nothing about the signature is taken on trust.'static there would demand more of the caller than the body needs.'static is right for a literal and wrong for anything else, so this is not a one-line default” — which had the direction backwards: where it is wrong the body is what says so, and the body is checked either way.?.m() says what the language has)shown("here".to_string()) into a String? comes out shown(Some(…)), and pair(1, 2) into two i64? parameters is told apart by position. Nothing of that record is unbuilt now.Type::method and a constructor’s is Type::new, and neither stands at the call.?.m() is refused with a sentence rather than built, and that is a decision rather than a shortfall: Part I 3.5 writes a field, and a form the specification does not name is not this compiler’s to add. It used to be “expected expression; found unexpected token )” — the () read as an empty parenthesised expression after the reach had already matched. The message now says what the language has instead: take the value with ??, or match on it where there is no fallback to give. Whether the language should have ?.m() is the owner’s question, and it is on the record as one.( for the reason the self-parameter arm consumes its colon: a fail here is high priority and not fatal, so it has to reach further than the alternative or the field arm’s reading wins.self)self is NK1119, which took a span of its own on FieldDef: without one the nearest span that walk had was a statement’s, on a different line, and a caret on the wrong line is worse than no message (ADR-051 D4). field_def takes @= now, FnArg takes a span too, and the caret lands on the field.self turned out never to have parsed, and NK1119 is not where it belongs. fn f(self: i64) is refused by the grammar — the receiver rule takes the word and the : i64 has nowhere to go — and what it said was “expected ); found :“. The arm that says it in a sentence is in receiver, before the bare self alternative. The first guess about this position was wrong and the record says so rather than being quietly corrected.peek(":"), or peek((KW_SELF ":")), the bare arm below reaches just as far and its “expected )” wins, because a fail here is high priority and not fatal: progress before priority, the same rule #235 recorded about the dsl block. So the arm has to get further than the alternative, and the cost is the caret, which lands on the type rather than on the word. The sentence is what carries the answer.?., and the fourth place the Some(…) goes)?. is built, so both halves of that section now work. x?.a lowers to x.map(|it| it.a) over a plain field and to x.and_then(|it| it.a) over one that is itself a T? — and that second one is the whole difficulty, because map there would give an Option<Option<T>> and a?.b?.c would come out reaching through a nullable of a nullable. Which of the two is a question about the declared type, so the checker decides and the emitter writes the word; the key is the statement and the field’s name, which is the shape fallible_methods already uses and documents.T? either way, which is what lets ?? end a chain and ?. continue one.?. through a value that cannot be absent is NK1121, with the plain . as the way out — a type that is not T? always has a value, and the language below has no map on it, so this was rustc’s refusal about the generated file. Asked only where the receiver’s type is known: refusing an unknown one would refuse a correct program.?. takes the value it reaches through, and the move error is now entirely in this language’s words. Option::map takes its receiver by value, so using the receiver again is a move — a rule the language below is right to hold (ADR-005), with a headline that names the program’s own variable on its own line. Its four notes did not: “Option::<T>::map takes ownership of the receiver self” is translated to ”?. takes the value it reaches through” — the explanation is the part a reader needs and the spelling is a shape this compiler chose — and “consider calling .as_ref()“, .as_mut() and “you can clone the value” are dropped, none of the three being a thing this language offers. Same rule that dropped the u32 remedy.User(name: …, home: home) where the field is an Address?. Keyed by the field’s own name, because a struct literal has one of these per field and a statement has only one span — and the shorthand User { home } is written out as home: Some(home), since a constructor has to go around the name.?. as one token, so the generator cannot insert the implicit whitespace inside it: a ? . b is not safe navigation, and a ?? b is the coalescing operator, which the arm cannot begin to match.rustc now, added after it caught the test author: a ?. on a plain value reached the backend and came back as “User is not an iterator” — reporting the wrong thing about a program this compiler would have refused.?.m() onto a method, which needs the method’s return type to pick between map and and_then (the ledger has it, so this is work and not a question), and the wrap at an argument, which is not a position this compiler can name.T? is a type, null is a word)&str? was a parse error and null was read as an ordinary name. T? lowers to the language below’s Option<T>, which is the mapping Part III 15.2 already wrote the other way round, and null lowers to None.?? was built — a ?? b is a.unwrap_or_else(|| b.into()) — an operation on an Option in a language with no way to write one. So the only values it could be used on came out of std, and the type that whole section is about could not be named by a program.? comes last, after the type’s arguments, so Vec[i64]? is a nullable list and Vec[i64?] is a list of nullables.Option is what it lowers to and not what it is called. A diagnostic says i64?; Option[i64] is a name no program can write, and a message using it would be a message about the generated file (Part III C.1). The ledger writes it the same way: (id: i64) -> i64?.?, on purpose. In the AST it is a flag beside is_view, because &str? is a nullable view and the two are independent — a wrapper there would have to be unwrapped by everything that asks whether a declaration names a borrow. In Ty it is Nullable(Box<Ty>), because every reader of a type already recurses, so a wrapper is one arm per reader instead of a condition inside each of them. The compiler then names the readers that forgot, which is the same bargain LitInterpolated made.T stands where a T? is wanted, and nothing stands the other way. That is the one widening this language has and Part I 2.3 is what asks for it (maybe_string = "World"); the other direction is refused, because that is the whole point of the type being separate, and ?? is how a program gets back.Some(…) is the compiler’s to write, and the checker says where. There is no Some in Nikaia and must not be, or the type’s purpose becomes paperwork. Whether the value beside the = is already nullable is a question about types and the emitter has none (ADR-028), so this is the shared_sites arrangement again: the checker records the statement, the emitter writes the constructor. Covered at an annotated let, an assignment and a return.add(3) is right wherever the parameter is numeric), so return 42 against a declared i64? answers unknown — and the type alone would have left the commonest case in the section unwrapped. Everything else is left alone, which is the fail-closed direction (ADR-010 D1): wrapping a value that is already an Option<T> would make an Option<Option<T>>.let m = null on its own is not refused, and that is C.4 rather than an oversight: let mut m = null followed by m = "hi" is a correct program, and this checker has no inference to tell it from the one where nothing ever says. null is a reserved word (ADR-051 D1), which keeps the other reading away — as a name it would be NK1117, and as a name something declared it would be a different program.crates/nikaia/tests/nullable.rs compiles what it lowers rather than comparing strings: whether the wrap landed in the right place is settled by the language below, and reading the emitted text would only say that this compiler agrees with itself.?., where the lowering is easy (x.map(|v| v.field)) and the decision is not — a field that is itself nullable needs and_then, or a?.b?.c holds a nullable of a nullable; and the wrap at an argument, which is not a position this compiler can name, since expressions carry no spans and the three covered places are each named by their statement.fn name() -> &str { "Ada" } is “missing lifetime specifier” from rustc, about the generated file. A view’s lifetime comes from the parser’s input (ADR-008) and is elided from a reference among the arguments; a function with none has nothing to elide from. Not about nullability and reproduces without it — &str? only made the signature one step longer and the error easier to read.rustc warning about the generated file reaches the user (#137). Part I 2.3’s example as the page writes it prints “value assigned to maybe_string is never read”. --explain translates an error back to the .nika line; a warning goes past it untouched. Not a matter of silencing it — the observation is true of the program and the writer should hear it.Send implementations and no message about an Rc<i32> in Rust’s words, and the section quoted one. What is built is NK2502 at both settings of user_parallelism (ADR-038 D7) and C.5’s third answer, undecided, for everything else. The claim is gone rather than left standing with a note under it: reading the metadata would be a different and larger mechanism, and nothing here needs it yet.i32, String and Option<T>, so nothing written down said which Rust type a Shared[T] is at a boundary — which is what ADR-045 §3’s whole argument turns on. One Nikaia type, two Rust ones: a Shared[T] that reaches a foreign call is an Arc<T>, because a call whose body this compiler cannot see may put what it is given on a thread of its own. The rest of the numeric surface, the view, Vec[T] and HashMap[K, V] are there too.no build starts is the I/O worker alone (ADR-038 D4) — but a Shared[T]’s owner count is atomic at both settings (ADR-037 D6), because the runtime touches it as well as your code and a count keyed on the switch would race with the machinery under it. Measured rather than argued: benches/refcount is what settled the cost.foreign-runtime.md and std-sysroot.md reason from what user_parallelism implied before ADR-037 D6; from-for-throws-and-touches.md, mutex-floor.md and rc-or-arc.md write their lambdas as sort_by_key fn { a }, which ADR-049 withdrew, so no sample on those three compiles as written. The analysis each records is unaffected — what a lambda’s argument is called changes nothing about when it runs or what it touches — and the note says to transcribe the reasoning and not the code. A laboratory record is allowed to be a snapshot; what it may not do is read as current.docs/README.md §1 makes a stale Status note a defect in its own right, because a reader cannot tell a plan from a promise.cache::sweep’s sort_by is a sort_by_key. clippy::unnecessary_sort_by turned trees.sort_by(|a, b| b.0.cmp(&a.0)) into an error, and CI had been red on it for two commits before anyone looked — including a documentation-only one, which is what says the failure was not the change under it.rust-toolchain.toml names stable and the container’s stable was 1.94.1 while CI’s was 1.98.1. A lint that arrives in a later stable is invisible to every gate run against an older one, so cargo clippy -- -D warnings passing locally says nothing about CI until rustup update stable has been run. Running the six gates is not enough; running them on the toolchain CI resolves stable to is.docs/open-decisions.md’s question of whether a name may be a reserved word. Twenty-nine words, written down in Part I 2.1 so a reader can see them and in parser::RESERVED_WORDS so a diagnostic can read them. rule NAME = not(digit) n:ident excluded nothing, and that was never a decision — the grammar is scannerless (ADR-001 D2), so there was no place a reserved list would naturally have lived and none was written.docs/error-corpus.md marked ○ parses turned out to be one defect, and the list closed all of them at once — which is the evidence that they were one thing. if { } read if as a variable and { } as a block; let 5 = x read let as a variable and then 5 = x as an assignment; if a = b { } was three statements. Each was a program that means something other than what is written, with no diagnostic of any kind — the worst class this project names, hiding in that file behind a ○ with the note that they were “things the grammar admits that probably should not be”. Two are refusals with token-soup messages now and one, D2, says expected `{` at the = and also loses the “open question” it carried: an assignment is not an expression.let fn = 3 lowered to let fn = 3;, refused by rustc with “expected identifier, found keyword fn” about a file nobody wrote (Part III C.1).dsl block gets its diagnostic by the categorical route, which is why this was taken rather than the alternative that entry offered. A cut — once dsl NAME { matches, forbid backtracking out of the rule — repairs one diagnostic for one construct; the reserved list repairs the class, and every keyword anybody adds later comes with it.rule, boundary, fold, par_fold and unchecked are keywords inside a grammar block and words a program may want everywhere else. A reserved word is one a reader of an ordinary statement could mistake for a construct, and no Nikaia statement begins with fold(.:: or a ., a reserved word is a name — one rule about where ambiguity can exist, not a list of exceptions. A segment follows a :: and a member follows a ., and no construct begins in either position, so Self::dsl (ADR-007 D5) and scope.spawn fn { … } keep their spellings. In the grammar that is SEGMENT: NAME without the check. Everywhere a name is declared or stands on its own uses NAME, and that is where the list bites.self is on the list and the grammar cannot enforce it, so NK1119 does. It is the one reserved word that is a name — self.min refers to it — and NAME is the rule both for declaring a name and for referring to one, so excluding it there would refuse every method body in the repository. The refusal moved to the declaration, which is the better place anyway: a parse error can say “a reserved word is not a name” and no more, and NK1119 can say what self already names. A parameter and a struct field are not covered, and the reason is checkable: neither FnArg nor FieldDef records a span, and a caret on the wrong line is worse than no message (#223).fail("…") arm does not work here, and this is the third time that has been established. ADR-022’s fn: … refusal uses fail and the shape looks right; measured, fail in this library records an error and lets the arm succeed, so NAME handed back an empty symbol and let fn = 3 lowered to let = 3; — a worse defect than the one being fixed. Reverted. fail belongs where a reading has to be replaced by a message, not where it has to be removed.seq, a DNA sequence in examples/k-nucleotide/src/main.nika, now dna; and from, a countdown’s start in tests/samples/while_loop.nika, now start. Worth stating as a number: a decision whose price is two renames in the only corpus that exists is a decision taken at the right time.panic::on_panic fn(info) sync { … } (Part I 7.2) was three statements and is refused at the sync. spawn fn { … } (Part I 8.2) was read first as a variable called spawn and later, once a trailing lambda could reach a path, as a call to a function of that name — two wrong readings, both reported by rustc, now one refusal reported here. Neither is progress on what those forms mean; it is progress on who says they do not work yet.assert c case this was first written around was already fixed and does not reproduce: KW_AS is "as" not(ident), so the s of assert fails the boundary. Measured rather than assumed, and recorded because the entry claiming it was live is what pointed at the question.assert cond, const X = 1 and unsafe { … } were each documented as “means something other than what it says”; measured, each parses as two statements and is then refused by NK1117 on the name. They are not reserved words and should not be — Part I 2.1’s list is what the grammar has a construct for, so a word held open for later work is a name today and joins the list on the day its construct does. quote { … } is the one that is still silent, because the undeclared name is the value of a let rather than a statement, and #235 now carries it as its own reproduction instead of a hypothetical.crates/nikaia/tests/parser.rs holds the Rust list and the grammar rule together by behaviour rather than by comparing texts: every word in the const is refused as a name, self is the documented exception, the sublanguage’s words are still names, Self::dsl still parses, and the three silent misreadings are refusals.let b = a + 1, where a is a constant an annotation declared an i32, was refused by rustc with “attempt to compute i32::MAX + 1_i32, which would overflow” — about the generated file, which is the Part III C.1 class. It is NK1116 here now, saying what the expression comes to.let answerable is the pin, and that is the whole of the design. A literal has no type of its own on purpose, so add(3) is right wherever the parameter is numeric — but an operand’s declaration does have one, and a + 1 is then arithmetic in an i32 whatever stands beside it. So the fold carries two things: the value, and the type an operand’s declaration fixed. Where neither the pin nor an annotation says anything, nothing is refused.i128, because a fold that wrapped would do quietly the very thing it exists to refuse — 9223372036854775807 + 1 has to be a number the message can name. Every step is a checked_ one, and None claims nothing: a mut local (this checker does not follow assignments, so the value it started with would be a claim about a program that no longer holds), a parameter, a for binding, a match arm’s name, a lambda’s argument, a cast, an operator outside + - * / % and a negation, or a fold that leaves the i128. Each stops the whole expression, and an expression that does not fold is never refused — which is the polarity the checker is held to (Part III C.4): it may fail to refuse a program rustc will, and it may never refuse one that is right.NK1118 is new. A division — or a remainder — whose divisor is a constant zero was rustc’s unconditional_panic, “attempt to divide 1_i32 by zero”, about the generated file: the same class, one operator over. It reads the same fold, so a divisor the compiler cannot evaluate says nothing and that division aborts at run time naming the Nikaia line (ADR-044) exactly as before. Recorded as D5.5, because D5’s sentence is “decidable where it is written” and D5 was written about a literal — and a literal is not the only constant.let b = 3000000000 + 1 — is still rustc’s refusal, because rustc has the inference that defaults it to an i32 and this checker has none. Refusing it here would also refuse the program that passes the sum to an i64.use http as h, and the two import forms get sentences)use http as h shortens the prefix, and a call, a type and a struct literal all reach through it — the one thing that record adds rather than refuses, and what makes the qualified-only rule affordable.use … as …. Every file’s items are in one crate root (ADR-047 D1), so a Rust alias there would be the whole program’s name for the package rather than this file’s. It happens in one place — Parsed::unaliased, on the way to every name — because the alternative was the same lookup at fourteen call sites in three modules, where the fifteenth would have been the one that forgot. It lives on Parsed because an alias is a name local to one file: resolving it is not name resolution in ADR-011 D2’s sense, it is reading the file’s own dictionary, and the dictionary is part of what was parsed.http::Request.method is private to http” where the file wrote h::Request: the type’s name is the package’s whatever one file calls it, and the use line connecting the two is at the top of the same file.use http as h beside use https as h is exactly the collision the alias exists to let a consumer fix.use http::{Request} and use http::* get sentences instead of a parse error at the brace — both are in every language that has them, so a reader will write one. Two things that needed: the arm comes first in the rule, because the plain arm matches use http and leaves the rest to fail as the next item at the same place with nothing to say; and it peeks the brace rather than consuming it, since a fail reports where it runs and consuming first put the caret under the line below.rustc’s “consider using the type u32 instead” is dropped from a relayed message. It was kept once, checked, on the ground that let x: u32 = 3000000000 compiles — and it does. What changed is ADR-048 D2: the numeric surface is the one Part I 2.2 names, and u32 is deliberately not on it. A remedy that works is kept; one that leads out of the language is not.let count = 42 // Compiler knows this is an i32; the type a number takes is the one its use asks for, and i32 only where nothing asks — so let n = 3000000000 is refused on its own and accepted where the next line passes it to an i64. Both programs are in the page now.rustc’s, and widening NK1116 to reach it would refuse the second — which is the one thing the checker may never do.overlap { … } reach the specification)overlap { … } is how a program asks for less: each statement in the block is a branch, the block waits for all of them, and its value is the tuple of their results in written order.overlap is not a task: the block ends before the function continues, so nothing outlives it, borrowing works, and the crossing check has nothing to do — which is what makes it lighter than two spawns and earns it a form of its own. The branches must meet on nothing and the compiler checks it, which is ADR-033’s analysis used the other way round: not “may I reorder these?” but “you said these overlap; is that true?”seq and the ordering switch with it (D1, D7). Two escapes were an admission — that the analysis can be incomplete, and that the correction is by hand — and a feature that changes what a program means, resting on that, is the shape of a defect source rather than of a guarantee.overlap on it, then the removals — because taking the automatic half away before there is a way to ask would leave the language with neither. Part I 1.2, 8.1.1 and Part III 13.3 all say so where a reader meets them, and #241 exists to say this is the thing not to pick up early.spawn’s runtime binding is what five records wait on..nika file the user wrote, and the compiler kept that promise everywhere it reported something — an abort at run time was the one path where it could not, because there is no compiler left to translate anything. An overflow, a conversion that does not fit, an index out of bounds and a written panic() now all read src/main.nika:2: the program stopped: attempt to multiply with overflow.const of (generated line, .nika file, .nika line), appended to the program — appended, because a table written above it would move every line it names, and correcting for its own height is a circle.#[track_caller] on std’s abort helpers. nikaia_std::index::at and the conversion checks panic for the program, so without it the location the hook is handed is a line of std’s own Rust — the same Part III C.1 defect one crate over.pub on a field means something, across a package boundary)fields = ["pub id: i64", "method: i64"] — and reaching or writing a field a package does not publish is NK1110. This was the hole with the shortest fuse the moment a second package existed (#247b): a type whose fields were private could be built by name from another package with nothing saying no.pub becomes pub and rustc keeps what it was not given; a field of a dependency is in the same crate, so pub on it buys nothing there (ADR-047 D2 rule 5 is what puts it in the same crate).r.method, and giving method a value in a struct literal. The second is the one that was silent. A type may still keep its fields private and offer functions — the test asserts the public field and the accessor both work, because the rule has to be a rule and not a blanket refusal (Part I, 9.3).pub is optional on the way in, so a ledger written before the word still parses — as a type with no public fields, which is the fail-closed reading (ADR-010 D1) and the one a stale file should get.TypeContract.fields is a named FieldContract rather than a tuple, because a third thing to know about a field is exactly when a tuple stops carrying its own meaning.http = { path = "../http" } in [dependencies] resolves, the package’s src/ is read as a namespace of its own, and its items become a pub mod http at the crate root — so http::serve() in Nikaia is http::serve() in Rust and nothing resolved it (ADR-011 D2). The first thing in this repository that is two packages.absorb qualifies the types inside an entry with the package’s name, and it can only do that for the types the ledger it is given declares — so a package whose Request is in one file and whose route(r: Request) is in another had to arrive as one ledger, or the signature kept the bare name and a caller writing http::Request was told the two were different types. That is the file-level defect fixed earlier in this session, one level up.use lines are checked against nothing and one that declares dependencies of its own is refused rather than flattened; two names for one path are refused, since they would be two copies of every type the package declares; a dependency’s [build] is ignored with a note, said once per build rather than once per pass; and a Nikaia dependency is part of the program, so it declares nothing to Cargo and ADR-043’s overflow check reaches it by construction — a test asserts that a library’s arithmetic aborts.NK1110 fires for the first time. Privacy is per package, so until a second package existed the check had nothing that could reach it: “secret is private to http” is a real diagnostic now, and pub means something a program can observe.use with a path in it says “use names one package and brings no name in” (D2); one naming no dependency says where to declare it (D4, read from the other side — an introduction that names nothing); and one name twice is refused (D5), checked before reachability, because a name written twice is a fact about the file and says nothing about whether either resolves.main.nika. What a package offers is its public surface; an entry point is what a program has.pub, so a private field can still be built by name from another package — the hole with the shortest fuse now), a package’s own package dependencies, use http as h, and everything a version would need.TrustedMap, so let m = HashMap::new() — a real defect in the program, reported against the right line — said “type annotations needed for HashMap<_, _, BuildHasherDefault<FxHasher>>“, naming a hasher this compiler chose (ADR-010 D5) and nothing in the program mentions. A name substituted on the way out is put back on the way in.map_name says “same table, same API, same full-content equality — so this is a name and not a translation”. Shared[T] is deliberately left alone even though the emitter substitutes it too — Rc and Arc are different types with different costs, and “expected Shared[T], found Shared[T]” would hide a defect in this compiler instead of translating one of Rust’s words. Both halves have a test.dsl block missing its } eod is reported as an undeclared grammar name, and the obvious repair was tried and reverted: a fail("…") arm is not fatal in this parser — its own documentation says “an error that got further still wins (progress before priority)” — so where the rest of the file parses, as it does here, the message is discarded with the attempt. What it needs is a cut in the parser library or a decision to reserve the word, and the entry now says so.a, b, c)a inside one was not a local but an argument. ADR-041 made that visible with NK1114, and visible is not the same as good — the rule stayed true for exactly as long as the form existed.Expr::Closure has no implicit flag, emit::implicit_params and check::warn_automatic_names are gone, and NK1114 is retired — its number is not reused, because a code that meant something in a published specification should not come back meaning something else. A fn { … } is a lambda of no arguments, which is what .or_insert_with fn { Stats(0) } always wanted.rustc. xs.map fn { a.id } — the idiom that existed until this record — now lowers to || { a.id }, which does not compile, so the message has to be this language’s and has to say what happened to the form: “a, b and c used to be a lambda’s arguments without being written down, and that form is withdrawn”, with the rewrite beside it. Keyed on those three names and only on them; the general case is now #141, because “this expression names something nothing declares” is a much wider claim than the statement rule makes.1brc, access-log, k-nucleotide and inventory, in two idioms: folding into a map entry and a sort key. The names are better than the letters were: fn (stats) { stats.add(m.temp) } says what is being added to.std’s names resolve was written only where the program had a use std::… of its own — which is not the same question (#144, the other half of the line fixed earlier). HashMap is a name the prelude provides and a program may write it without importing anything; such a program lowered to a file where TrustedMap — the name a trusted input’s map gets (ADR-010 D5) — was undeclared, and rustc said so about a file nobody wrote.#[allow(unused_imports)] the same rule already asked for: what a program uses is not a list the emitter keeps, and it does not need one — the import is one line, and a locally declared name shadows a glob. std itself is the one exception, because it is the prelude; that is emit::emit_std, which says so rather than being detected.cargo run -p nikaia --example dump works again. Its default user-parallelism was still 0, a count, which ADR-037 D2 refuses — so the command a failing fixture test tells you to run did not run.Vec of them, a struct holding one, an impl handing views of self back, the whole thing behind a Shared — every one lowers correctly and runs. A suspicion nobody can reproduce is a claim about this compiler that nothing supports.std’s names resolve was written only where the program had a use std::… of its own — which is not the same question (#144, the other half of the line fixed earlier). HashMap is a name the prelude provides and a program may write it without importing anything; such a program lowered to a file where TrustedMap — the name a trusted input’s map gets (ADR-010 D5) — was undeclared, and rustc said so about a file nobody wrote.#[allow(unused_imports)] the same rule already asked for: what a program uses is not a list the emitter keeps, and it does not need one — the import is one line, and a locally declared name shadows a glob. std itself is the one exception, because it is the prelude; that is emit::emit_std, which says so rather than being detected.cargo run -p nikaia --example dump works again. Its default user-parallelism was still 0, a count, which ADR-037 D2 refuses — so the command a failing fixture test tells you to run did not run.Vec of them, a struct holding one, an impl handing views of self back, the whole thing behind a Shared — every one lowers correctly and runs. A suspicion nobody can reproduce is a claim about this compiler that nothing supports.std’s names resolve was written only where the program had a use std::… of its own — which is not the same question (#144, the other half of the rule fixed earlier). HashMap is a name the prelude provides and a program may write it without importing anything; such a program lowered to a file where TrustedMap — the name a trusted input’s map gets (ADR-010 D5) — was undeclared, and rustc said so about a file nobody wrote.#[allow(unused_imports)] the same rule already asked for: what a program uses is not a list the emitter keeps, and it does not need one — the import is one line, and a locally declared name shadows a glob. std itself is the one exception, because it is the prelude; that is emit::emit_std, which says so rather than being detected.cargo run -p nikaia --example dump works again. Its default user-parallelism was still 0, a count, which ADR-037 D2 refuses — so the command a failing fixture test tells you to run did not run.Vec of them, a struct holding one, an impl handing views of self back, the whole thing behind a Shared — every one lowers correctly and runs. A suspicion nobody can reproduce is a claim about this compiler that nothing supports.Error: and ten frames of this compiler: the emitter refusing a dsl that names a grammar nobody has, the manifest reader refusing an unknown [build] key, the project driver refusing a missing src/main.nika, and the CLI refusing a backend nobody has.format!: refused! is written where anyhow! was and refuse! where bail! was, mirroring that pair exactly so a call site changes by one word.rustc wrapper invoked with no compiler named, and reading the sysroot’s own files. Those are this compiler’s problems, and the frames are then the most useful thing on the screen. A test drives four different modules through the CLI with RUST_BACKTRACE=1 set — which is the case the rule is about — because what is being tested is that the rule is not one path’s habit.i64, and the numeric surface is the one the page names)xs.len() hands back an i64, and an index takes one. The two conversions are not symmetric, which is the whole argument. Out of a length cannot fail — a usize exceeds an i64 only above eight exabytes of single-byte elements — and the user wrote it in every loop, sum and comparison that met a length. Into an index can fail, for a negative number, but a negative index is an access out of bounds, which Part III A.2 already aborts on: not a new failure mode, the same one a step earlier. So the trade is a great deal of visible ceremony that cannot fail against one invisible abort that already exists — and the user now writes no conversion at all, in either direction.xs.len() becomes xs.len() as i64; an index becomes xs[nikaia_std::index::at(i)], where at chooses on the type of what is in the brackets — an integer becomes a usize, and anything else, a map’s key most of all, goes through untouched. It has to be a trait and not a function because this emitter does not know types (ADR-011 D2) and counts[path] is an index too. A negative index panics as “index out of bounds: the index is -1”, which D1 requires: as usize would have said 18446744073709551613 instead, a number nothing in the program contains.xs[0] and &text[1..3] need no conversion — Rust’s own inference gives a literal the usize a sequence wants — and they cannot take one: at(0) has nothing to infer its argument type from, and cannot infer type about a generated file is what Part III C.1 forbids.usize::truncating_i32 and usize::truncating_i64 go with it: nothing narrows out of a type nobody can hold. check’s NUMERIC — the list that decides which conversions are checked at run time — is now i32, i64, f64, u8; the machine-width types were in it because len handed one back. #257 is gone with them, the question removed rather than answered.u8 is named in Part I 2.2 (D2), with the same conversion and arithmetic names as every other numeric type: fs::read hands back a Vec[u8], so a program that reads a file already met one and what was missing was the specification admitting it. u32, u64 and isize stay out until a program asks. The page now also says which types the compiler accepts without offering, so the list of types is the list of types.as: a written cast, truncating_i32, and a length. Rust reads such an expression wrongly in exactly two positions — beside a unary operator (-x as i64 is (-x) as i64) and as the left of a comparison, where x as i64 < k is read as i64<k> and the program does not parse — so parentheses go there and nowhere else. truncating_ used to parenthesise always, which is the other half of C.1: let n = (big as i32); is a Rust warning about a file nobody wrote.std cache stops filling the disk)std tree is a whole Cargo target directory of some 240 MB, and the key naming it holds the compiler’s own fingerprint — so every rebuild of the compiler started a new one and nothing ever took an old one away. Measured: 1.7 GB in one user cache, 13 GB in the directory the project tests share, and a build failing on “no space left on device”. Coexisting is right (ADR-021 D7 — dimensions coexist, they do not share); coexisting forever was the defect, now recorded as D12.5.--sharing, --overlaps and --trust work on nikaia build and nikaia run (#210). They were on the single-file path only — nikaia build --sharing answered unexpected argument — which is exactly where the asking is not done: --sharing exists because there is no way to request the cheaper reference count, every fallback is enumerated instead, and “that is only fair if the fallbacks can be asked about. This is the asking.” A person with a real program builds it with nikaia build. They report over every file of the package, against the package’s own ledger rather than each file’s, and one function serves both paths so the two cannot disagree.Shared handed to a public parameter of a function another file declares kept two counts, exactly as a field did — found by a test written for the flags above. The floor is therefore about any slot another file owns, not about fields, and it is read off the union-find rather than off the recorded handles: a slot another file owns has no handle in this run, and joining to it is the only trace of it there is. Fallback::ForeignField is Fallback::ForeignFile.println(f"{hold(c)}") handed a handle to a function the analysis never saw, so nothing joined and nothing forced. A hole is Nikaia source (ADR-032 D3) and the type checker has walked holes since that record; any analysis that stops at a literal is one a hole can be hidden in..nika beside the entry takes part, whether or not anything names it, and they share one namespace: no use between them, no prefix on the names they share. The emitted Rust is one crate root with every file’s items in it — no mod, no use super::*, nothing to resolve, because one namespace here is one namespace in the language below. The entry goes first, being the file that may declare main, and the rest in file-name order, so the output is a function of the tree rather than of a directory listing (Part III 13.5).rustc would report it as a duplicate definition against a file nobody wrote (Part III, C.1).use is refused, and which mistake it is decides the message. One that names a file beside this one says the files of a package already see one another and to remove the line, because the shape that used to work is the one most likely to be written; one that names anything else says use names another package and that depending on one is not built. use std::… is untouched.pub is visible throughout its package — and NK1110 therefore fires on nothing at all. The check is built and the boundary moved out from under it: a qualified name is a name from another package, and depending on one is not built. It is the one code in Part III C.3’s catalogue with a check behind it and no program that reaches it, and the day a package arrives is the day pub starts to mean something a program can observe.nikaia.toml declares; examples/ is a directory of eleven programs that each declare main, and compiling one must not pull in the other ten.examples/inventory is the change made visible. It was this repository’s first program of more than one file, written with use stock and stock::total(…); it is three files of one package with neither. The comments explaining what a module boundary bought were rewritten rather than kept as an account of a rule that is gone.use does, and what a package is)use makes a unit of code reachable and brings no name in — not by a glob, not by a braced list, not one at a time. The blanket form is regretted everywhere it exists and settles itself; the selected form is the real question, and it is refused on a different ground: it is everywhere and withdrawn nowhere, so it is not a mistake. What decides it is a rule this language has already applied to itself — one form, not two spellings of it (ADR-041 withdrew the automatic lambda argument names on exactly that ground). A selected import creates that situation for every name in the language. And the refusal is not irreversible, which is why it can be firm: adding the form later breaks no program, while a question left open is one every file written in the meantime has to live with unanswered. use x as y shortens a long prefix, a prefix must be introduced, and one name per file.use between them; what the package offers outward is whatever says pub, in whichever file it is declared. This removes the re-export problem instead of patching it — there is nothing to re-offer, because the names already share a space — and a library’s internal file layout stops being its public surface by construction. Java, Go and Rust all put that boundary above the single file; Python puts it at the file and __init__.py is the consequence. Part I 9.2’s privacy boundary moves from the file to the package, and two files of one package may not declare the same name.std already does, so this generalises a mechanism rather than adding one. No registry, no name space, no distribution format — the three ADR-002 D1 §5 refuses to guess at stay open. Five rules come with it or they get decided by accident, and the fifth is the one that would have been decided wrongly by silence: a Nikaia dependency is part of the program and not a foreign package, so ADR-043’s overflow check reaches it — otherwise a library computes silently wrong numbers while the program calling it aborts.NK1110 still says “private to utils.nika“. The half of the machinery a package boundary needs is built one level down — a qualified name from another unit resolves to one type.let c: pool::Conn = pool::make() was NK1103, with a help line asking for exactly what was already written — a module could hand out behaviour and not data, which is most of the reason to split a program. One spelling was missing: the ledger keys a type pool::Conn, while a module’s own signature says -> Conn because that is how the file declaring it writes the name. Ledger::absorb now qualifies the types inside an entry as well as its key — it is the only place that knows both the module and what that module declares — so the two spellings are one name before anything compares them.pool::Conn(id: 1) was a parse error, because the literal’s name was parsed as NAME where a type’s is type_name. Both are type_name now, and a foreign struct’s fields are read from the program’s own ledger by the exact key — never by suffix, which is how std is matched: two modules may each declare a Conn, and a suffix match would answer with whichever came first. A wrong field name, a wrong field type and a wrong annotation are now all refused by their qualified names.Shared in a field another file declares kept two different counts (#143), and fixing the above is what uncovered it: the analysis runs once per file, so pool.nika saw the field and not the value while main.nika saw the value and not the field. Measured, in one generated file: pub db: std::sync::Arc<Conn> beside let c: std::rc::Rc<pool::Conn>, which rustc refused — about a file nobody wrote. The answer is the polarity this analysis already runs on rather than a new rule: where it cannot prove that nothing crosses, it does not lower. Such a field keeps the atomic floor as Fallback::ForeignField, a seventh row in the enumeration --sharing prints, because a fallback nobody can ask about is not a fallback this design allows.true asfoo read as true as foo, returnx as return x, assert c as let c = true as sert; c; — the cast took sert for a type name — and forx in 0..3 as a loop that binds x, which is a valid program with a different meaning from the one written. Every word keyword now carries a boundary (parser::KW_*): one rule per keyword because the generator has no parameters, and UPPERCASE, which is not a style choice — a lowercase rule is syntactic, so the implicit whitespace would go between the word and its boundary and as i32 would be refused for having a space in it.NK1117, in this language’s words. That is what was left of the class: assert, unsafe and the _000 of 1_000 are read as names, a name on its own is a legal statement, and each of them used to lower and be refused by rustc about a file nobody wrote (Part III, C.1). Part I 2.2 is deliberate that 1_000 is 1 beside a name — the reading is right, being accepted was not. Four things count as declaring a name, and anything the compiler cannot see is a name it does not refuse, because refusing a correct program is the one thing it may never do (C.4).fn { … } has no parameter list, so its body walked with a in scope nowhere — harmless while nothing asked whether a name is declared, and a false refusal the moment NK1117 does. The answer comes from emit::implicit_params, the same function the emitter writes the parameter list from, so the two cannot disagree about it.anyhow::Error out of main, so after the diagnostics had said their piece the terminal also got Error: 1 type error — and where RUST_BACKTRACE is set, which is a normal thing for a developer to have set, ten frames of nikaia::project::check and below. diagnostics::Refused marks a statement about the program; the tally stays and the envelope goes. A parse error travels the same path, which needed one more fix: modules.rs formatted the error into a string to prefix the file name, and formatting loses the type that says it is a refusal.unused import: super::* for a use item this compiler writes itself, and the translation recognised it, said “no Nikaia source maps to this”, and printed it anyway — telling the reader to remove a line they never wrote. Both halves are fixed because either alone leaves the hole: the emitted import carries #[allow(unused_imports)], and a warning that maps to no Nikaia line is not reported at all. An error with nowhere to put it still is: a warning suppressed costs nothing, an error suppressed leaves a build that failed with no reason given anywhere.user_parallelism and answering “may not” everywhere — so Part II 12.2’s counter, the program user_parallelism = yes exists to serve, could not be written. At yes a real operating-system lock stands in the emitted program, under which that counter is entirely safe, and it was refused on account of an implementation that does not occur in it. There are two destinations now and each gets an answer that is the same at both settings, which is the whole of what ADR-005 §1 Group B asks: the justification may differ per setting, the answer may not.yes because a real lock is underneath and several threads are what it is for, at no because the task is interleaved on the same thread and nothing crosses at all. Two reasons, one answer. Locked[T] and SharedMut[T] are answered by what they hold there, like a container: a lock makes nothing crossable that was not, which is Group B’s transitivity rather than an exception to it.yes; it is refused anyway, because a lock’s Rust type differs per setting while a Rust library has one signature, so a library written against one would compile at one setting and fail at the other. The message says the answer was chosen, so that somebody tripping over it in two years can see a decision behind it rather than an oversight — and the way out is not “don’t do that”: the caller opens the lock and hands the value inside it over, which is the shape an ordinary function already has (ADR-042 D1).NK2501 and NK2502 stop sharing one verdict, which ends ADR-037 D6’s coda: it said that after D6 no type answers “may not”, and the lock answers it at a foreign call. So NK2502 has an occupant and Part III C.5’s “built, and no program reaches it” goes with it, while NK2501 refuses nothing a Nikaia program can write.Ours, and this record did not decide that for it. contracts::order asks the same verdict about a signature’s result, because overlapping two statements puts each inside a closure that hands its value back. That closure is this compiler’s own, on this compiler’s own thread, so it is the destination a task is — and a pair whose result holds a lock overlaps now where it kept its place before.contracts::send’s own header wrote down came due, and the second column turned out to be the destination. The header required every name in its tables to be safe both to move to another thread and to be looked at from one, and said that the day a name belonged in one column and not the other the file would need two. A lock is exactly that name — at no it may be moved and may not be looked at — and it never has to be asked that way, because both destinations answer before the distinction matters. The columns stay one; the table splits by destination.SharedMut[T] nor Locked[T] is a type the backend can lower (ADR-039 §4), and a program that writes one as an annotation is checked here and then fails to emit. The verdict is the half worth having early — it is what a library’s signature is written against, and it is what 12.2’s counter was blocked on.Bytes joins the table, Mapped derefs to it, and a page mapped once outside the handler is a body that costs a reference count per request. The handler answering with it duplicates the handle rather than moving it, which is ADR-040 D1’s rule applied to the one handle that record did not name — without it a captured body would answer exactly one request.http::File("index.html"), whose bytes go from the page cache to the socket without entering the process. It is a response body and not a file handle — std::fs keeps the surface ADR-019 argued for and gains nothing. Its length and any failure to open it are settled before the status line, because after the headers are written there is no status code left to send (D6).std’s choice and not the program’s (D3), and the argument for it is prior art rather than this project’s benchmark. Apache httpd’s EnableSendfile and nginx’s sendfile are off by default — Apache’s having been on in 2.2, so a project with two decades of deployments moved that default the other way; nginx’s sendfile_max_chunk bounds one call for fairness rather than throughput; HAProxy’s kernel splicing is opt-in with a fallback to an ordinary copy; lighttpd’s server.network-backend names the mechanism as a configuration value. Not one of the four puts it in what an application writes. ADR-038 D5’s runtime file gains send-file, default auto and pinnable — the shape all four arrived at.sendfile loses at 64 KiB” was one busy afternoon of five runs — in the other four it is the best vehicle in its family there. “The crossover is near a megabyte”: there is none to find on a quiet box. “splice is worst at every size”: at a megabyte it is mid-field. What reproduced in all five is what the record now rests on — reading per request is worst everywhere, mmap per request is 2–3× worse than plainly reading below a megabyte, a kept mapping wins at 4 KiB by ~2×, and sendfile is never worse than reading. docs/history/zero-copy-send.md §2.1 lists what was retired and §5 the prior art, with its sources.io_uring’s Splice is not the mechanism, and the number that says so is one an easy benchmark would have got backwards (D4), at the two sizes where it reproduced. A pipe has to stand between a file and a socket, which is a second transfer that no batching removes: 78, 89 and 955 µs of machine CPU against every other vehicle’s less, with both hops submitted as one linked pair and the pipe widened first. Its sending thread meanwhile pays a flat ~21 µs at every size, because the work is done by kernel workers no per-thread accounting sees — so the losing mechanism looks like the winner by a factor of twenty on the instrument most benchmarks reach for. The machine’s busy time is the column this record is stated in.rustls takes bytes and produces bytes (ADR-038 D2), and bytes that never enter the process cannot be encrypted; HTTP/2 interleaves DATA frames, so there is no single run to hand the kernel. Both serve the file through D1’s mapped path instead, and kTLS is named as unbuilt rather than promised. On wasm32-unknown there is neither sendfile nor a mapping, so the file is read — the surface is the same and the mechanism is the target’s.std::http does not exist (ADR-038 §4.5), so neither the row nor http::File has anywhere to be; the order that record gives is unchanged — a socket layer that keeps registrations, then a minimal HTTP/1.1 server, then the parsing moved into Nikaia, then rustls, then HTTP/2. A send_file in nikaia_std::rt could be built ahead of all of it and is not: it is the mechanism that loses at the sizes a server sends most.mmap per request costs 76.0 µs of machine CPU at 4 KiB against a plain read’s 30.5, two and a half times worse, because a mapping is a page-table edit and not a pointer. So D3 forbids the obvious reading of D1: std may not implement http::File by mapping per request. Where nothing may be kept, sendfile is the mechanism — 28.0 µs at 4 KiB and 49.5 at 64 KiB, best or tied at every size, and the only one whose cost does not include a copy into the process.std keeps one — bounded (static-cache in ADR-038 D5’s runtime file, 0 disables it), dropped when the file’s identity or modification time moves, which is a correctness property and therefore has no key that turns it off, and never a promise a program may depend on. A page that must be held for certain is mapped by the program itself, outside the handler, which is D1 and needs none of this.Untrusted and may not reach the filesystem unchecked (D7). ADR-010 D8 built a taint lattice and named three consumers needing their own decision: hashing was D5 of that record, escaping was ADR-017, and path traversal was the third and open one. Handing request.query("file") to http::File, fs::map, fs::read or fs::write is a compile error rather than a runtime check — a ../../etc/shadow that arrives as a 200 is not a failure a status code fixes afterwards. What clears it is fs::within(root, name), whose answer is trusted where its argument was not and is none where the join would leave the root, or trusted: true at the source with its site in nikaia.contracts. It needed no new analysis: the taint travels the join that is exactly how a traversal bug is written, because ADR-010 D1’s lattice already rides ADR-008’s edges — which is the argument D8 made for building the mechanism generally, arriving as evidence.contracts::trust exists and nikaia --trust prints what it found; what is missing is a diagnostic where an untrusted value reaches a path parameter, and fs::within beside it. Testable against fs::map today, and every program that later writes http::File inherits it.EnableMMAP off there — which no invalidation check can prevent, because the fault arrives first; so D8 gains a fourth property and keeps a mapping only under a root the operator named, which is where D7’s fs::within already puts one. And sendfile blocks on a cold page cache: nginx needed SF_NODISKIO and a thread pool to keep that off a worker, and at user_parallelism = no there is one thread for everything the user wrote, so a blocking call there is not a slow request but every other connection stopped. Named now so that whoever builds D3 does not find it in production.benches/sendfile/ is a workspace member and docs/history/zero-copy-send.md is the write-up — the method, the machine, the spread, and the four things it does not measure, named rather than folded into the recommendation: a slow far-away client (where sendfile needs a descriptor and the alternatives need the body addressable per connection, which is a memory argument this bench cannot see), an encrypted connection, a cold page cache, and a cache that evicts — the bench’s keeps all 64 mappings forever and never asks whether the file changed, which is the correctness work D8 prices and it does not. The whole block was run twice and every ordering reproduced except the megabyte row of the second family, where three of four vehicles sit inside each other’s spread; no claim rests on it beyond the kept mapping winning, which both runs agree on.A specification example wrote a postfix ?? the language does not have, found by extracting Part III 17.1’s own download route and handing it to the compiler. Part I 3.5 defines ?? as null coalescing — a ?? b — and nothing else; lookup(q??) is expected expression; found ')'. It is not a rule specified ahead of the compiler, because those carry a Status note and this has none: it is an example using a construct no section defines, which a reader cannot look up when it fails. 17.1’s example is rewritten to use what 3.5 has; ADR-018 D3 writes the same form and stands, because an ADR is written once and the correction belongs to whatever decides whether the nullable gets a postfix unwrap at all. #198 is that question, and spec-promises.md gains the probe.
fn handle_request(req: http::Request) throws IoError names an error type, which ADR-023 D1 forbids and which the parser rejects by name — the one program in this repository most people read, and it was never run through the compiler it advertises. Three more: req.respond(200, data) is not surface (ADR-018 D2 has a handler return what answers the request), the file reader is fs::read_to_string, and .route("/", handler) is the form the trailing lambda of ADR-022 replaced. What is there now is Part III 17.1’s own server with a query parameter added, and it lowers.async, no await, no .unwrap(), no lifetimes, no Arc::clone” describes other languages, and a comment explaining spawn’s polymorphism explained a runtime that does not exist. What replaces both is two properties stated positively and a status note saying std::http is not built. The example count below it was stale too: eleven of twelve run, and inventory/ was missing from the list.i32 overflow while every benchmark here runs --release, where it wraps (ADR-043 D1, D6). The measurements were taken on wrapping arithmetic and the shipped program aborted — two different programs, and nobody chose that. The specification was silent on overflow, which is a gap and not a contradiction: Part III A.2 already had the category (“errors indicating an inconsistent program state”) and an overflow is the same kind of thing as the division by zero beside it. A.2’s list gains it, so Part I 1.2’s rule — you choose how when you build, never what — now holds for arithmetic too.Cargo.toml, on for the program and off for every dependency. Not by a checked helper per operation, which was considered and refused: the arithmetic is identical either way, but anyone who opens the emitted Rust during a benchmark comparison and finds a helper call where + should be will not believe the numbers. A dependency’s hash function wraps on purpose and is not this compiler’s code to be right about, so the rule stops at the crate boundary. overflow-checks is deliberately not in Profile, which holds what the build switches and the codegen table decide; an overflow aborting is what the language means, so it is written unconditionally and there is no key that turns it off.crates/nikaia/tests/overflow.rs): the emitted Rust aborts with the check on, and says a + b; the manifest this compiler writes for a real project carries both halves; and — #[ignore]d, like every Cargo-driven test here — a nikaia run of an overflowing program aborts end to end. Run once to confirm the seam: it does.-1, shifting wider than the type); wrapping_* and saturating_* by name, with no operator for either, because a sign would have to be spelt with & and that is borrowing here; as, which turns out to be unspecified altogether despite being in the parser and used four times in examples/1brc.nika; and that a literal out of range is a compile error rather than an abort.Measured while writing the Status note rather than asserted. 5000000000 as i32 prints 705032704 — it truncates silently, so D4’s check is real work and not a formality. And let x: i32 = 3000000000 is already refused where it is written, but by rustc and in Rust’s words, down to the lint name overflowing_literals and the advice to use u32: the position is this language’s and the message is not, which is the Part III C.1 class.
as is defined to truncate, so there is no check to switch on — measured, 5000000000 as i32 prints 705032704 under -C overflow-checks=on and under -C debug-assertions=on alike. So a narrowing conversion is emitted as a checked one instead, and the test compiles with -O and no check flag at all, which is the claim: a conversion carries its own answer and takes none from the build. The objection that refused helpers for the arithmetic does not reach here — a conversion is rare and written down on purpose rather than standing in every loop, and the checked form is what a Rust programmer would write at that spot anyway.as i32 narrows or widens by what it is given, and the emitter resolves no types (ADR-028) — so the checker records (statement, target type) where every such conversion in the statement narrows, and the emitter looks the pair up. The fourth use of that arrangement, after fallible_loops, fallible_methods and shared_sites. The list is written the other way round — which conversions always fit — because a pair nobody thought about should be checked rather than truncated in silence.truncating_i32 is how a program says it wanted the low digits (ADR-043 D7), after the pattern of the wrapping_ names: the operation in the prefix, the destination in the name, because there is one method per destination and the ledger has to write a signature for each. It lowers to Rust’s as and to nothing else — in parentheses, because as sits between the unary and the binary operators in Rust’s precedence while a method call sits above both, so -x.truncating_i32() has to keep meaning -(x as i32).f64 the old conversion was silent three ways — 1e20 as i32 was the largest i32, -1e20 as i32 the smallest, and “not a number” was 0 — and Rust offers no try_from for it, so nikaia_std::num carries the test. A count is as wide as the machine is, so text.len() as i32 is checked for the reason the overflow decision exists at all: otherwise the behaviour hangs on where the program was built. An integer to an f64 is the one that stays unchecked — 9007199254740993 through an f64 comes back 9007199254740992, there is no sensible place to stop, and that is written down in Part I 2.2 as a limit.i128, and the obvious spelling was wrong. Comparing against the target’s limits as f64 accepts a value one past what the type holds, because i64::MAX as f64 rounds up — and for a machine-width type the constant would have to differ per machine. f64 as i128 saturates far outside every target’s range, so a saturated value cannot land back inside it and try_from then answers exactly, at whatever width the machine gives the type. “Not a number” is asked first, because it is the one case no range test catches: it compares false against everything and as turns it into 0..expect appends the error’s Debug, so the message carried TryFromIntError(()) — Rust internals in a sentence a Nikaia user reads, the same leak the diagnostics filter exists to stop; it is a panic! with the sentence now. And the std helper’s abort named num.rs rather than the generated line, which would leave ADR-044’s table nothing to translate: #[track_caller] fixes it, but only with the panic! in the function’s own body — inside a closure handed to unwrap_or_else the attribute does not reach, measured.keep(db), where keep takes a Shared[Connection], lowers to keep(db.clone()), so the name outside keeps working and each handle dies at the end of its own block (D3). Unconditionally, and not only where the name is used again (D2): a line further down may not decide what a line further up does to a cleanup point. Only where the argument names a handle — a variable, or a field of one — because a temporary has no block of its own to die at the end of and so produces no second owner. .clone() rather than Rc::clone(&x), because it is right under either count and so cannot disagree with the type the position was lowered to.serve(&db) and a &Shared[T] parameter are both handed on as written; nothing steps the count, and the tests check both directions in one program.--sharing names each duplication site beside the count it printed (ADR-040 D5). D1 leaves the place the atomic instruction is paid unwritten in the source, so it has to be readable somewhere, and the count was already printed there. A value that was only ever borrowed says so — the absence of a step is what a reader came to look up. The site names the callee and the position it takes the handle in, and not a line: an Expr carries no span in this AST, which is the limit check::Checked::fallible_methods already records about a method call.keep(pool.db) gives the callee an owner as surely as keep(db) does, and the field’s slot was left unjoined — so the field’s class and the parameter’s could disagree about which count they are, and the emitted Rust would hand an Rc where an Arc is wanted. That is a rustc error about a file nobody wrote, which Part III C.1 forbids, and it is the fail-open direction docs/history/rc-or-arc.md §8 warns about.spawn body using a handle and names it as a duplication site; spawn does not lower, so no emitted program reaches it (Part II, 11.2).Shared[T] is a type a program can write and run)Shared[T] exists. std.contracts carries [type."Shared"] and one method entry, Shared::deref with signature = "(&Shared[$T]) -> &$T"; the backend lowers the type to std::rc::Rc<T> or std::sync::Arc<T>; and the annotated let of Part I 6.2 makes the first handle — let db: Shared[Connection] = connect("…") becomes Rc::new(connect("…")). There is no Shared::new and there must not be: the annotation is the constructor (ADR-040 §3). A field whose declared type says so is the other place one is made, and a call site is neither — NK1115 refuses that and points at the line where the sharing belongs.emit::Emitter::map_name sees only a name and could not answer it, so contracts::sharing computes one count per slot over the whole unit and the emitter looks it up — the third use of the arrangement check::Checked::fallible_loops and fallible_methods already have: computed where the knowledge is, looked up where the code is written. A count belongs to the allocation, so every position of one union-find class carries the same answer and the types on both sides of a call agree by construction; a position nothing decided gets the atomic floor.serve(&db) works through the deref entry and nothing new. ADR-042 D2 saw a transparent container through to what it holds as soon as its ledger recorded a deref, and D2’s own note said Shared would be seen through “the moment it has a deref entry, with no further decision”. It is, and the receiver’s arguments bind $T (ADR-031), so one line answers for every Shared a program writes. A borrow duplicates nothing, which is ADR-040 D1’s correction.let is an allocation this analysis watched being made. contracts::sharing forced such a handle to the atomic count as Fallback::UnseenOrigin — “it came out of a call” — which was true while the only way a Shared could arrive was from somewhere else, and is false of the line that makes one. It is answered yes only where something written down says the value is not already a handle: a literal, a struct literal, or a call whose ledger result type is known and holds no Shared. A result of ? is the absence of a claim and is answered no, which keeps the polarity the file runs on. Without this, the base case ADR-037 §5 asks for — a Shared nothing crosses with really does get the plain count — was unreachable for every program that actually writes one.Rc and Arc differ in nothing a program can observe except speed and Send-ness (docs/history/rc-or-arc.md §2), so a test that read the emitted Rust would pass on a lowering that does not run. crates/nikaia/tests/shared.rs lowers, compiles with the rustc that built the test, runs the binary and compares what it printed — at both settings of user_parallelism, which is the claim D6 rests on.a_std_entry_that_takes_a_shared_needs_a_second_look refused any std signature mentioning Shared, because the analysis treats a described callee as accounted for. Shared::deref mentions it and is the shape the guard is not about: it takes &Shared[$T] and hands back &$T, so there is no handle for the entry to keep and nothing for it to cross a thread with. It now refuses a position that takes or hands back a handle, and a borrow of one is exempt.sysroot::lower_std_module said Shared was what would break lowering std at one setting, citing ADR-037 D3’s per-build Rc-or-Arc. D6 took the representation off the switch, and the count a value gets never reads user_parallelism, so a Shared in std’s .nika half lowers to the same bytes at both settings.& on a generic type answered nothing, so every view of one was unchecked (ADR-042 D1). view_of produced a type only where the type had no arguments, so &Vec[i64], &HashMap[&str, Stats] and — the day it exists — &Shared[Connection] all came out as “nothing is known”, the program was emitted, and a mismatch was reported by rustc about the generated file: expected reference &str / found reference &Vec<i64>, with a Rust #[warn(unused_variables)] note beside it. That is what Part III C.1 forbids, and it was not a gap in what the checker knew — the type was there and a step threw it away. A view now keeps its name and its arguments; a view of a view is the view; &String stays the one rewrite to &str; a tuple and a lambda still answer nothing, deliberately.fs::Mapped that “a parser cannot tell the difference” and std.contracts spells it — fs::Mapped::deref is (&Mapped) -> &str — but nothing consulted that entry, so a function taking a text view refused a mapped file, by this compiler’s own NK1102. A type whose ledger records a deref is now seen through, once, and only to rescue a comparison that has already failed: it can turn a refusal into an acceptance and never the reverse, which is what makes it safe in a checker whose discipline is to say nothing where it knows nothing (ADR-024). The receiver’s arguments bind the signature’s variables as for any other method (ADR-031), so an entry written (&Shared[$T]) -> &$T will answer with what this container holds.serve(&db) duplicate a handle, when a borrow hands nothing on and there is no atomic instruction to pay. Duplicated where the handle is passed by value; untouched where the inner value is lent out. Not a wording preference: it decides whether a function that only uses a shared value touches the count.let db: Shared[Connection] = postgres::connect("…"), no constructor and no method, because the language asks for sharing to be visible in the source and there it is, in the line where it starts. Only where the shared type stands in the same line may a plain value become a shared one; never at a call site merely because a signature wants one, since a call in which the word does not appear would move the cleanup point silently and there would be no saying whether the value was handed on or duplicated. NK1115 says so and points at the line where the sharing belongs. Part I 6.2 also gains the borrowing rule and the SharedMut form — there is no & straight through a lock, so the caller opens it and passes the borrowed value in, and the called function must be sync and must touch no lock. All of that is specified ahead of the compiler: none of the three types exists, so NK1115 is catalogued and never raised, while the borrowing half is built and exercised by fs::Mapped.fn(user) { … } is what a lambda looks like, and fn { … }’s automatic a, b, c are carried as experimental (ADR-041). The automatic spelling hides a rule that is not visible in it: how many arguments the lambda takes is read off which of the three names its body mentions, so a local called a is not a local but an argument, and the closure’s arity stops matching its call. examples/access-log/src/main.nika carried a comment warning its own reader off c for exactly that reason; the comment is gone because the trap is, not because it was documented. NK1114 (warning, so the program still compiles) fires where one of the three is actually reached for, and names the mechanical rewrite — the same letters in a parameter list, after which the count is written down instead of read off the body. A fn { … } that reaches for none of them is not warned about: the form is not being withdrawn, the automatic naming is. The warning asks the same question the emitter asks to write the parameter list, from the same function (emit::implicit_params, now shared), so the two cannot disagree. Part I 5.2 and 5.3 swap the two options over, Part II 12.1/12.2/12.6, Part III 13.5/17.1 and six sites in four examples/ programs are rewritten with names that say what the argument is, and ADR-022 D1 is amended in which spelling is recommended and in nothing else — there is still one form with two spellings and both still parse everywhere. What this buys is the freedom to change the automatic rule at all, which is impossible while every lambda depends on it; whether it is eventually withdrawn is not decided.The site offers System, Light, Dark and Sepia, and remembers which. The control is in _layouts/default.html and the palettes are in assets/css/nikaia.css — one block of custom properties per theme, and below it the list of places a palette reaches, written once each. jekyll-theme-primer states its colours as literals, so a second theme could not be a variable swap inside the theme; it is a second stylesheet that names the theme’s own places and reads their colour from a property. System is the absence of an attribute on <html> rather than a value of it, which is what makes a reader who has never touched the control, a reader with no JavaScript, and a reader who chooses System land in the same state. A stored choice is restored inline in the <head>, before the first paint, so a reader who chose Dark on a Light system does not see the white page first.
Sepia is there because of what this site is: three specification parts and thirty-nine decision records, read end to end rather than searched. It is a light theme with the white taken out, and it reuses the light token colours rather than carrying a third set — which is only possible because the code block’s background is taken off the block and given back from the palette.
The 180 ```nika blocks are highlighted from editors/vscode/syntaxes/nikaia.tmLanguage.json. Jekyll highlights with Rouge, Rouge has no lexer for Nikaia, and the consequence was on every page of the specification: the Rust and TOML beside a Nikaia block were coloured and the Nikaia was not. What was missing was never a grammar — that one exists, and editors/test/ has tested it against vscode-textmate all along — but somewhere to apply it outside an editor. scripts/site-highlight/highlight.mjs runs Shiki, which tokenizes with the same library, over _site after Jekyll and before the upload. What VS Code shows and what the site shows now come from one file and cannot drift apart.
Every block, not just the Nikaia ones. 279 of them on 52 pages — 181 tokenized as Nikaia, the 180 ```nika fences and the one ```nikaia — and the rust, toml, sh, bash, markdown and text blocks too, so that one mechanism carries the reader’s theme and no block is left on Rouge’s fixed light colours. A fence naming a language Shiki does not know is set as plain text rather than failing. Both themes are written onto each token as custom properties with no literal color, because an inline colour outranks every selector in the stylesheet — that one decision is what keeps nikaia.css free of !important and lets the picker recolour code without a second copy of the page.
It asserts the shape it depends on instead of assuming it. The input is generated HTML in two shapes — the wrapped form Rouge produces and the bare <pre><code class="language-nika"> it produces for a language it cannot read — and the script fails the build if any <pre> reaches the end unconverted, rather than publishing a page where one block is themed and the next is not. That check earned itself immediately: it caught the block in docs/history/technical_notes.md that sits inside a list item, where Kramdown indents the wrappers and the two </div> are not adjacent.
Verified by building the site the way pages.yml does and driving it in Chromium: all four themes, prefers-color-scheme in both states with no stored choice, the choice surviving a reload and a navigation, and the narrow-viewport layout where the control stops being fixed and becomes the first thing on the page.
Every page was a bare fragment: no <head>, no stylesheet, no viewport. _config.yml has named jekyll-theme-primer since the site was set up, and assets/css/style.css was built and served on every deploy — 145 KB of it — with nothing on any page linking to it. The cause is that the site is deliberately made of Markdown files that carry no front matter, and a page that names no layout gets none; the theme was never reached. The defaults block added to _config.yml is what names it. The second entry in that block takes the layout back off assets/, where the theme’s own style.scss carries front matter and would otherwise have been published wrapped in a page.
The logo was a 5 MB PNG named .jpg, on the front page. 1768075880760.jpg is 2816x1536 and is not a JPEG at all — browsers sniff the bytes, so it worked, and it cost every visitor five megabytes to see an image the README shows 300 pixels wide. nikaia-logo.jpg is the same picture at 600x327 and 24 KB, a real JPEG, with height on the tag so the page does not jump while it loads. The original stays in the repository and is no longer published: it is excluded, which takes the built site from 8.8 MB to 3.7 MB.
The README now links to the site it publishes. A docs badge and a Documentation site entry in the header navigation, both pointing at https://keywan-ghadami.github.io/Nikaia/ — there was no way to reach the published documentation from the repository’s front page.
 inside the centred <div> is Markdown inside a block-level HTML element, which GitHub renders and Kramdown does not. They are <img> now, which both render identically. It was there before and invisible, because there was no styled page to see it on.
spawn has one spelling, and the clone goes outside the task)spawn fn { … } is the form in all three Parts. Part III Appendix C.5’s NK2501 example wrote spawn({ total(counts) }), which made two normative documents disagree about the shape of the construct they were both describing. spawn is a call whose last argument is a lambda — the trailing lambda Part I 5.3 already has, and the same shape as the lock’s blocking doors — so the parenthesised spelling is gone. It was the only site of that shape: a sweep of the three Parts, every note in docs/, README.md and manifesto.md for a lambda argument written inside a call’s parentheses turns up one other line, Part II 12.8’s supervisor::start_link(fn { … }; restart_policy: "always"), and that one is left exactly as it is — the lambda there is not the last argument, so there is no trailing form to move it to without deciding an argument order the specification has never written. It is the owner’s. The entry in docs/spec-promises.md says which half of its finding this closes and which half — the parser’s spawn "(" expr ")" rule, untouched — still stands.
Three examples cloned inside the task, where the clone does nothing. Part II 11.2 states that a spawn body’s values are moved in when the task is built, and its own bullet says to call .clone() before spawning. Part I 8.3’s example, the help: text of NK2101 beside it and the second branch of Part II 12.7’s NK2102 help each wrote the clone inside the body instead — where it copies the task’s own copy, leaves nothing for the parent, and in Part I’s case carried a comment claiming the opposite. All three now bind the copy before the spawn and hand the task the copy. Nothing in crates/ changed: NK2101 and NK2102 are catalogued and never raised, so the wrong help text existed only in the specification, and no test asserted it.
Status note, which it needed and did not have: the move rule is written ahead of both halves it depends on — spawn does not lower and NK2101 is not reported — and the parser still wants parentheses around the body.docs/automatic-clone.md records the open question that the hand-written clone above invites, so that it is not mistaken for a comfort feature. A clone is one more owner and therefore a later cleanup, which is the observable change ADR-037 D3 refuses to infer for Shared and what D7’s rule sorts as written rather than inferred; ADR-005 §3 rejects compiler-inserted clones outright and ADR-008 D5 re-argues the ban for the view case. The note decides nothing and says what a proposal would have to answer.Six sentences were false or incomplete and are now the rule as ADR-039 decides it. Part II 12.3’s “often a compile-time error” is always (D2); Part II 12.2’s safety-net sentence claimed the re-entrant case is not refused statically and is replaced by the refusal plus the check’s new role, self-control rather than error handling (D2, D8); Part I 1.2’s “exactly two things to choose” is three (D8); Part I 6.2 defined Locked[T] by the nesting that no longer exists (D9); Part I 6.3’s “you must use the .access() method” is four doors (D10); and Part II 12.2’s derivation from the sync lambda is replaced by the stronger sentence — while the lock is open in get and set, no code of the user’s runs at all (D10).
SharedMut[T] is the shared mutable type, from the start. Part I 6.2 now says what each of Shared[T], SharedMut[T] and Locked[T] is for — Locked[T] keeps its own use, individually locked fields inside a shared structure — and the hand-written Shared[Locked[T]] nesting is gone from every example in Parts I and II. Nothing is renamed: no program could write either type.
The four doors are in Part II 12.2, with the beginner’s view in Part I 6.3, each with the one sentence that says why it is shaped that way, plus the two refusals that come with them (assignment names set; a set whose argument reads the same container names update). Part II 12.2’s ordering rule is refined rather than contradicted: get is a read and the other three are writes, so two gets are unordered (ADR-033 D2) and the writing forms keep their order.
The lock-touching property is written down in Part II 12.3 — a second derived property per function beside sync, inferred over the same call graph, which is what catches chains and self-calls — together with why it is coarse, the warning never to answer 12.2’s ordering question with it, and the rule that a function handed outward is judged by its body and never by its type. Part II 12.7 adds that opening a scope inside an open lock is refused, because the scope waits; Part II 11.2 adds that taking a lock inside a spawned task is the ordinary case, because it does not.
Eight Status notes say what is built, and nothing in this entry is. Part I 1.2 (the third switch), 6.2 and 6.3; Part II 11.2, 12.2 (two of them), 12.3 and 12.7 — four of them new, four corrected. Neither type is known to the checker, no door exists, no function carries the property, and no refusal is reported. Part II 12.2’s old note claimed the front end accepts Locked[T] and hands it to the backend; the probe in ADR-039 §1 shows the name is an error where it is written, and the note now says that.
No rule was invented beyond the record. ADR-039 D6’s foreign-call rule extends Part III 15.2’s existing crossing sentence and is left to Part III. The diagnostics appendix needs codes for D2’s nesting, D6’s foreign call and D10’s two refusals; the rules are written without a code until Appendix C allocates them.
cargo test --workspace 532 passed, 0 failed (unchanged — no test reads these pages); cargo fmt --all --check clean; scripts/check-adr-refs.py exit 0. Specification only: no .rs file is touched and no behaviour changes.
docs/stored-views.md: an assessment, and it decides nothing. What is built refuses a stored naked view or writes the subject’s buffer out; the full rule is ADR-008 D2’s lattice, under which a stored view is neither an error nor a signature change but a representation change — the buffer takes a handle, the view becomes an offset and a length, and nothing is refused at all. The note asks what stands between the two, item by item.
Of the seven things it would need, two are present, one is computed and thrown away, and four are not. Present: which fields of a type hold a view, and which parameters a result may point into — the latter from the signature only, so borrows(a | b) says nothing about which one. Computed and discarded: the checker infers a type for every let and Checked exports only findings, loops and method calls; the channel for handing one more answer to the emitter already exists. Absent: which parameter of a std call keeps what it is given, a buffer variable per view position, a solver over them, and any representation other than a native reference.
The decisive limit is that there is one buffer to name. crates/nikaia/src/emit/mod.rs has a single INPUT_LIFETIME, and contracts/trust.rs already states the consequence in its own header. An analysis that proved a view came from a different buffer than its destination’s would have nothing to emit, so it could only refuse — which is what the increment above already does for every destination it cannot name.
A half-built version was built and measured, which is why the note has a recommendation rather than a hunch. The obvious next step — letting a local whose declared type holds a view be a destination — refuses examples/k-nucleotide/src/main.nika: 32 files lower instead of 33. That program writes views of its parameter into a map it hands back and is correct, because the container’s buffer is the parameter’s, and what says so is the return. No amount of following where the view goes answers that; equating two buffer variables does. So widening the reach before the solver exists converts working programs into refusals, and the order is forced. The probe was reverted and the corpus is back to 33/22.
Also recorded there, because it is a different defect and was left alone: a free function taking a view-holding struct by value and handing one back fails with E0106 whether or not the view is stored — the naked view is not what breaks it, so NK2302 would be the wrong code to report. Nothing in the corpus writes the shape.
No new ADR, no decision, and no change to any program: this entry is a note and a docs/README.md line.
One step further along the same question, not a second mechanism. NK2302 asks where a naked view parameter’s view goes. A field is declared, and its declaration says which buffer it points into (ADR-008 D1) — so where the destination is a field of a subject that itself holds a view, the buffer is already named and the parameter is lowered as a view of that buffer rather than refused. fn record(&mut self, name: &str, temp: i32) on a Summary holding HashMap[&str, Stats] now emits fn record(&mut self, name: &'a str, temp: i32), which compiles and runs — it is the program the E0621 above was about. Part I 6.6’s Status note and Appendix C.3’s row say which cases are which.
The cost of the fail-closed direction is now a narrower signature rather than a refusal. self.names.contains_key(name) was refused in the entry above, because nothing written down tells a call that keeps what it is given from one that does not. It lowers now, with name written as a view of the subject’s buffer — the undecided case is still decided the safe way, and what the safe way costs is what a caller may pass instead of whether the program builds. NK2302 therefore refuses no program the language below accepts.
Measured both ways on the corpus, which is the only reason this is here and not reasoned about. Nothing that lowered before is refused now: 33 lower and 22 do not, the same files as before, and the emitted Rust is byte-identical for all 33 — no program in the repository has the shape, so the change costs the corpus nothing in either direction. Nothing that was refused before lowers now either, for the same reason. The direction is shown on four programs in crates/nikaia/tests/stored_views.rs instead, each compiled with the rustc that built the test and run: the stored-into-a-field form, the stored-through-an-opaque-call form, the read-only call the entry above refused, and the one that reaches the field through a local. Reading the emitted text would not have caught this defect, because what the old behaviour produced was text that looked right.
Where the reach stops, as three tests rather than three sentences. A function or method whose own subject holds no view still refuses, even when the destination is a field of a struct that carries one — there is no buffer in hand to write. A view handed back through the result still refuses: covering it means writing the result’s buffer too, which is a second edit to the signature. A view given to a task still refuses. The caller side is unchecked: writing the subject’s buffer on a parameter narrows what callers may pass, and nothing in the corpus hands such a method a shorter-lived view.
cargo test --workspace 532 passed, 0 failed (529 before, 3 added and 2 converted from refusals to lowerings); cargo fmt --all --check clean; cargo clippy --all-targets -- -D warnings clean; scripts/check-adr-refs.py exit 0. nikaia.contracts is unchanged: the ledger records Nikaia signatures, and &str is still what the source says. No new ADR.
A function taking a naked &str and storing it lowered to Rust that rustc refused with E0621, naming the generated file — which Part III C.1 calls a bug in this compiler rather than in the program. fn record(&mut self, name: &str, temp: i32) on a Summary holding HashMap[&str, Stats] emitted fn record(&mut self, name: &str, …) inside impl<'a> Summary<'a>, and the reader was told help: add explicit lifetime 'a to the type of name — every noun of which is something ADR-008 D1 says the author never writes. The 0.0.9 entry below recorded this as D7’s open half and said closing it needs ADR-008 D2’s tether lattice; what it actually needed first was for Nikaia to have an answer of its own, and NK2302 is that answer. The struct form — fn record(&mut self, m: Reading), which examples/1brc.nika has used all along — was never broken, and is what the message shows.
NK2302 (Part III, C.3; the rule is Part I 6.6). crates/nikaia/src/views.rs asks, per naked view parameter, where its view goes, and reports four destinations: a field reached from self whose declared type holds a view, a field of a struct the function builds, the result where the result holds a view of a buffer that is not this parameter’s, and a task’s body. One refusal per parameter, at the place that says the most about where the view went.
It is wider than the rule, and that is the polarity rather than an oversight. Where a carrier of the view is handed to a call on the subject, nothing written down says whether the callee keeps it — insert does, contains_key does not, and std.contracts records key: ? for both — so the call is reported as keeping it. Failing open there means emitting Rust that does not compile, which is the C.1 violation this exists to remove; ADR-010 D1’s “an analysis that fails open is a vulnerability generator” is the same argument with a different cost. The consequence is named in the code, in Part I 6.6’s Status note and in C.3’s row: self.names.contains_key(name) is refused today. NK2302 is therefore the one code in Appendix C not covered by C.4’s property, and C.3 says so.
The domain is narrow on purpose, which is not the same thing. Only destinations whose buffer is known not to be the parameter’s own count. examples/k-nucleotide/src/main.nika’s fn count(seq: &str, k: usize) -> HashMap[&str, Tally] writes views of seq into a map it hands back and is correct, because the result points into seq and nothing else; the result case asks exactly that question — is this parameter the only buffer the signature names — before it reports anything. A local whose declared type holds a view is not a destination, because deciding it needs to know which buffer each local views, which this compiler does not compute.
Measured, not assumed. Every .nika in the repository still lowers: 33 lower and 22 do not, the same 22 as before (21 deliberately broken files in tests/errors/, plus examples/fortunes.nika, whose dsl postgres has no lowering). no_program_in_the_repository_keeps_a_naked_view walks examples/, benches/ and the Nikaia half of std and finds nothing. cargo test --workspace 529 passed, 0 failed (520 before, 9 added); cargo fmt --all --check clean; cargo clippy --all-targets -- -D warnings clean; scripts/check-adr-refs.py exit 0. No new ADR: the decision this rests on is ADR-008 D1, which already says a view in a field is a marker and a parameter gets no annotation.
Found on the way and not fixed, because it is a different defect. A free function that takes a view-holding struct by value and hands one back — fn record(s: Summary, name: &str) -> Summary — emits s: Summary<'_> … -> Summary<'_> and fails with E0106, whether or not the view is stored: with a second naked view in the signature there is no single input for the result to be tied to. Nothing in the corpus writes that shape, and nothing here touches it: the naked view is not what breaks it, so NK2302 would be the wrong code to report.
fn(user) { … } inside a call’s parentheses and only fn { … } outside them, so Part I 5.3’s own example, Part II 12.3’s account.access fn(to) { … } and access_all(a, b) fn(x, y) { … } and Part II 12.7’s task::scope fn(s) { … } were each read as two expressions - the call missing its last argument, and a closure passed to nobody, which in Rust type-checks often enough to be a different program rather than an error. trailing_lambda now shares closure_params with closure_expr, named arm first, and a trailing lambda reaches a path as well as a method call. ADR-022 D1’s “one lambda form” is unchanged and not reopened: naming the arguments is that form spelled out. The Status notes in Part I 5.3, Part II 12.3 and Part II 12.7 say so, crates/nikaia/tests/lambdas.rs compiles the emitted Rust for each shape, and the three re-run rows in docs/spec-promises.md correct what the 0.0.9 entry below reported about them. Still not built, and now tests of their own: an effect marker on a lambda (fn(info) sync { … }, Part I 7.2) has no grammar at all, and spawn fn(x) { … } is one call to a function named spawn where it used to be two statements naming a value - the same E0425, one noun closer to the truth.Mutex floor for Locked would cost against Part II 12.2)An investigation, and it decides nothing about Locked[T]’s representation. Part II 12.2 gives the lock two implementations, one per user_parallelism, and forbids the program pausing while something is locked — so a Mutex floor at both settings has to be cleared against that rule first. Four things were found, all in docs/mutex-floor.md. (1) Blocking is not pausing, and the specification already says so where it had to choose: the panic hook must be sync and Part I 7.2 permits it to block (“briefly”), ADR-006 D6 calls that “exceptionally permitted”, and ADR-005 D6 is titled “No suspension point while a lock is held” — so a floor neither violates 12.2’s rule nor sidesteps it. (2) Whether access stays sync turns on one ledger line and never on the primitive: run against the real inference (crates/nikaia/tests/lock_floor.rs), sync = "from(f)" makes 12.2’s idiom sync = "inferred" up every caller and a lambda that does I/O still loses it, while an entry with no sync line costs the idiom and, by ADR-027 D1’s greatest fixpoint, every caller above it — which would make 12.2’s own idiom uncompilable in 12.6’s own vehicle. (3) At user_parallelism = no the floor costs 11.3 ns an access and no syscall: Mutex<i32> against RefCell<i32>, uncontended, +11.24 / +11.40 ns and ×6.18 / ×6.06 over two runs, falling to ×1.05 with 400 multiply-adds inside the guard (a fixed cost, not a proportional one), zero futex calls in 1 000 000 acquisitions under strace -c, and Mutex<i32> is smaller than RefCell<i32> (12 bytes against 16). A lock at no can never be contended by user code — one user thread (ADR-037 D2) and an I/O thread that cannot name a user value (ADR-038 D4’s Op enum, re-checked) — so that is the whole of the cost there; the contended row is the expensive one (multiples, and the only row whose ratio did not reproduce) and it is unreachable at no. (4) What the floor really costs at no is a diagnostic: 12.2’s no implementation exists to panic on a logical deadlock, and a Mutex hangs instead — run on one thread, exit 101 with RefCell already borrowed against killed-at-timeout, which at no is the whole program and also stops ADR-006 D5’s cleanup deadline, the hazard class D5’s “honest limit” already names for blocking FFI. Keeping the panic costs +2.0 ns, ×1.15 (an owner check in front of the acquisition), so losing it is a choice rather than a consequence.
The representation is already decided in the specification at both settings, and in no ADR at all — which narrows the open question, but not where it was expected to narrow. ADR-037 has no panic table and does not mention Locked; the table naming Locked[T] poisoning at yes is Part III Appendix A.2, and Part II 12.2 already names a RefCell-with-reentrancy-check at no and a real OS Mutex at yes in plain words. The only ADR that names Locked[T] is ADR-005 D6, which decides the rule and records the divergence without deciding it. So an always-Mutex floor is not filling a gap: it changes a normative sentence, which under docs/README.md §2 is a record that names what it displaces. The poisoning claim is asserted rather than argued (benches/lockfloor: a panic inside the guard poisons the mutex for every later acquisition and leaves the borrow flag recording nothing), and it is the one argument for the floor that is about capability rather than cost — Arc<RefCell<i32>> is not Send, so a Mutex floor is the only shape of Locked under which docs/history/rc-or-arc.md §7’s blocked path reaches 12.2’s own counter.
Part II 12.2 gets a Status note, because what it said was not true. The section states its no-I/O-while-locked rule as compile-time behaviour and shows the NK2201 it reports. Locked[T] is unbuilt in full: the type name and access are accepted by the front end and reach the backend, which answers “cannot find type Locked in this scope”; nothing raises NK2201, so the program 12.2 shows as refused compiles today. Found on the way, and not about the floor: 12.2’s idiom is not sync today either — counter.access is a method call no ledger can resolve, so ADR-027 D2’s polarity refuses the claim, and the same is true of a pure par_iter body. The word can still be written over an access, because the check is deliberately permissive about a method call.
What was built for it, and what was not. benches/lockfloor/ (five paired shapes, every repeat printed, the machine and load printed by the script, and four assertions on cargo test for the differences a program can observe) and crates/nikaia/tests/lock_floor.rs (four assertions putting both spellings of a hypothetical Locked::access entry to contracts::sync::infer). No Mutex reached the emitter and no representation was chosen. cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0.
NK2605: an explicit call to something that can fail, in a function that does not declare throws, was not reported at all — and the committed ledger carried the wrong fact. fn ruft() -> String { return liest() } beside a throws liest lowered in silence and nikaia.contracts recorded [fn."ruft"] signature = "() -> String" with no throws, where an absent throws in that file means it cannot fail. That file is committed, shipped with a package and compared by --locked (ADR-020), so the defect was not a missing caret but a false fact published about the one thing a caller cannot see. NK2601–NK2604 and NK2701 are all about calls nobody wrote (ADR-025 D1), and the written call — the case that rule was generalised from — had no code. Part I 7.1 states the rule and carries the worked example; Part III C.3 has the catalogue row.
The ledger stops publishing it because the program is refused, and not because throws became inferred. ADR-023 D1 derives the error set and leaves the declaration in the source (“in source, throws is bare”); ADR-025 D1 already decided the case in words — “the function must declare throws, and the compiler says which implicit call is the reason”. Inferring the keyword would also need to know about catch, which that walk does not: fn f() { g() catch { … } } cannot fail, and an inference blind to that would publish the opposite false fact. project::lower checks every unit before it calls Ledger::render, so a refused program renders no ledger — asserted, with the ledger as the subject, by the_ledger_is_never_published_for_a_caller_that_does_not_say_it_can_fail.
The polarity, asked the way ADR-027 D4 asks it, and the answer is the mirror image. A declared throws whose body cannot fail keeps ["?"] — an over-claim, the safe direction, and it still does. The absence of a declaration outranked a body that provably could fail, and kept “cannot fail”: the same shape as the hole D4 left for sync, where an assertion outranked a contradicting body, with the sign flipped — an over-claim there, an under-claim here, both fail-open. docs/history/from-for-throws-and-touches.md §7 is the table.
Found on the way, and fixed with it: ADR-023 D8’s propagation was not lowered at all. A written call to a throws function came out as Ok(liest()) — a Result inside an Ok — so adding the throws that NK2605 asks for produced a second rustc error about a generated file, which would have moved Appendix C.1’s violation one step later instead of removing it. Every program in examples/ writes catch at the call, so nothing in the corpus ever asked for the ? and no test did either. It is emitted now for a call by name, suppressed inside a catch’s guarded half, and a_written_call_propagates_its_failure compiles and runs the result. A method call still does not take one — the emitter has no receiver types (ADR-028) — so NK2605 refuses a fallible method call and catch is the shape that lowers; Part I 7.1’s Status note says so, and ADR-023 §6’s table gains the row.
contracts::throws now uses ADR-028’s method resolution, where only contracts::sync did. ADR-031’s chain — HashMap[&str, Stats] → and_modify’s fn(&$V) → a.add(v) → Stats::add — gives record throws = ["ZuVoll"] where it read ["?"], which is what ADR-028 means by one answer to what a.add(v) goes to. It is the only change here that makes an answer narrower, so it is argued rather than assumed: every contribution that went came from a call the compiler named; an unknown receiver and a method no ledger describes still arrive as "?" through MethodCalls::unresolved; and what replaces the "?" is a written-down fact, because an absent throws in std.contracts means “cannot fail”, reviewed like code and contrasted with an absent touches in that file’s own header. This corrects the 0.0.9 Answered entry below, which reasoned from “contracts::throws answers every method call with "?"”.
throws with a type is refused with the reason, and the four specification sites that wrote one are corrected. The decision is ADR-023 D1’s own title, so nothing was decided here. fn f() throws IoError gave a parse error at the type name offering -> and sync as alternatives; kw_throws now carries a fail(…) saying the set is derived and recorded in nikaia.contracts, on the precedent of ADR-022’s removed fn: … form in the same file. Part I 6.4 wrote throws IoError once in code, twice in prose and once inside NK2601’s own help: — the compiler’s suggested fix telling the user to write what the compiler refuses — and Part II 10.2 B wrote throws ParseError. All four now read as Part I 7.1, which was right and is left alone.
E0621 is in ADR-005 D7’s enumerated classes, where a missing explicit lifetime belongs beside the seven other lifetime and borrow codes; docs/history/nightly-cost.md §3.3.2 found it absent. What “translated” means for it is recorded rather than glossed: crates/nikaia/src/diagnostics maps the place for every code, so the message already landed on the .nika line, and the text is still rustc’s — help: add explicit lifetime 'a to the type of key, whose every noun is something ADR-008 D1 says the author never writes. That is the same open half D7 records for E0277, at a second code, and it is stated and not solved: closing it needs ADR-008 D2’s tether lattice, which is not built.
Two notes said the repository “named” a nightly, and it does not since ADR-001 D1. docs/history/foreign-runtime.md §1 and docs/history/runtime-cost.md §1 keep rustc 1.94.0-nightly (8d670b93d 2025-12-31) — the version is a measurement and rewriting it would falsify one — and say instead that it is the compiler the block was run under and not a toolchain this repository names. ADR-036 §2 is unchanged deliberately: it names the compiler beside its instruction counts and makes no claim about what the repository uses, so there is no tense in it to correct.
Checked rather than assumed: cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0. No correct program started being refused: every .nika in the repository still lowers, and no_program_in_the_repository_has_a_type_error walks all of them. No new ADR.
Shared gets is inferred, and only ever downwards)One rule, and everything is downstream of it: it may only ever take an atomic away. D6 makes the atomic count the floor; contracts::sharing is an optimisation on top of it. It may lower a particular value to a plain count where it proves nothing crosses a thread with it, and where it cannot prove that the answer stays atomic. It may never add an atomic and it may never make a program unsafe — the same sentence read from both ends. The prototype docs/history/rc-or-arc.md left behind is now the real thing, under that rule.
contracts::send and contracts::sharing have a contract with each other and it runs one way. send answers may for a Shared of crossable data, and that is only true because the count is atomic — so a value sharing lowers must not cross. Every crossing is a seed, and a missed seed is unsoundness rather than a missed 9 ns.
Step 1 created a seed that did not exist before, and it is the one the experiment explicitly left out. docs/history/rc-or-arc.md §4 declined to seed task::both — the crossing the compiler chooses for statement overlapping — on ADR-033 D4’s polarity, and that was sound only while a Shared was MayNot and never overlapped. Since D6 an overlapped operation may run on a thread of its own and hand a Shared back across one. It is a seed now, through the fallback for a handle whose allocation this analysis did not watch being made.
Five more fail-open siblings of the prototype’s one known bug, found by asking what else “we have already seen it” could be hiding in. docs/history/rc-or-arc.md §8 records a <struct>.<field> slot guarded on “already known”, which made the verdict depend on the order somebody wrote their functions in. Its siblings: a handle read back out of a field produced a fresh class nothing forced; a handle captured by a lambda handed to a call nothing describes was not forced, because only a bare Expr::Variable argument was; the same for a handle inside a tuple or any other expression; a handle handed over as an option after the ; was never looked at; a handle assigned into a field was not joined to it; and a Shared behind a public field of a public type, or behind a public parameter that merely holds one, was left plain. Every one of them is a test, and the field cases run in both written orders for the reason the original did.
The sharing summary is the fifth ledger column — sharing = ["counts | <result>: plain", "hits: atomic"] — beside sync, throws, touches and borrows. Two facts in one line, and the first is the one a caller could not work out for itself: which positions are one allocation, so they have one count whichever side of the call decides it. docs/history/rc-or-arc.md §5.1 names it as the summary that composes, and the union-find that decides a count computes it already — which is why this is a column and not a mechanism (ADR-020 D1). It is also the only column in that file that cannot make a program wrong: the floor is safe, so an entry that says atomic about everything describes a correct program paying 9 ns. inference becomes stage0-signatures+sync-bodies+throws-bodies+sharing-bodies, because one more column is read off the bodies, and 13.5’s paragraph about that header is also brought up to date — it still claimed stage0-signatures+sync-bodies, two columns ago.
nikaia --sharing is an explanation and not a switch, in the shape ADR-033 D9 gave --overlaps and ADR-010 D7 gave --trust: a heading per function, one line per value with what it got, and the reason under it. Then the closed list of six fallbacks, with the ones this program hit marked and the remedy for each — which is the part that makes D8 fair rather than merely decided.
There is deliberately no way to ask for the cheaper count (D8), and the reasoning is recorded because it will be proposed again. Every fallback is one of two things. Either a missing contract — a call, a method or an argument position nothing describes, a type whose fields are unrecorded, a value whose origin this analysis cannot follow — and then the remedy is to write the contract, which is ADR-033 D4’s own closing argument that std.contracts growing is how programs get faster. Or it is a public signature, where whether the value ever crosses is decided by callers who do not exist yet, so an override would be a promise about somebody else’s code — which std.contracts already refuses to make about a higher-order method’s sync, and which a representation cannot be spelled from(…) for at all, because Rc<T> and Arc<T> are two types and a published artifact has already chosen one. D9’s conclusion carries: the language’s escapes are restrictions, not permissions, and a request for a cheaper representation is a permission.
The rule that does all of it with nothing new: what is observable is written; what is not is inferred, with safe as the floor. Shared is written, because sharing changes when a value is cleaned up (D3’s own reason, unchanged). Rc against Arc is inferred, because docs/history/rc-or-arc.md §2 measured that nothing a program can observe distinguishes them. Locked is not decided by any of this: RefCell against Mutex differs in what a reentrant access does and in what a panic inside one leaves behind, so it is observable, so it is written and not inferred.
What the analysis rests on is named rather than assumed. A call the ledger describes is treated as accounted for, which is exactly what ADR-005 §5.2 already does for the same question — a contract is an account, and std’s are reviewed like code. The residual is ADR-005 §5.3’s: enforceable only as far as a foreign crate is honest. Today the exposure is zero, because no std signature mentions Shared at all, and a_std_entry_that_takes_a_shared_needs_a_second_look is what makes somebody look on the day one does.
The base case is still the test that makes the rest worth anything. A Shared that nothing crosses with gets the plain count, a struct field with nothing crossing keeps it, a call inside the unit keeps it, and a field read with nothing crossing lowers — without those, an analysis that answered atomic for everything would pass every other test in the file. Thirty tests in crates/nikaia/tests/sharing.rs, up from fourteen.
Checked rather than assumed: cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0. Nothing is emitted: Shared still has no constructor and no Rc::new/Arc::new, so no count is ever allocated — sharing::rust_name says which two Rust types the two answers are and is called by nothing. contracts/order.rs is untouched.
Shared is one atomic count, at both settings)D3’s open question is closed, and the number in docs/history/rc-or-arc.md is what closed it. D3 expanded Shared from user_parallelism — Rc at no, Arc at yes, per build — and the new D6 narrows exactly that row: the count is atomic at both settings. The reason is one sentence. NK25xx’s verdict is about a type and may never consult the switch (ADR-005 §1 Group B, §5.3), so a type whose expansion moved with the switch had to be refused a crossing at both settings — which made Part II 12.2’s Shared[Locked[i32]] counter, the program the switch exists to serve, unwritable. The price is the measured +9 ns per clone-and-drop pair: ×3.40 on a loop that is only the count, ×1.01 on a loop with ~70 ns of work in it, and ×1.00 on a handle that is read and not cloned. The expensive atomic is the contended one (an order of magnitude, ×11–×17) and no choice avoids it — a count several threads touch has to be atomic.
In send.rs it was the one line the experiment said it would be, and the line cost more around it than in it. Shared moves out of the MayNot case and into CONTAINERS, where it is answered by what it holds — so Shared[Vec[i64]] crosses and Shared[Locked[i32]] is undecided, for the lock’s sake rather than the count’s. No switch reaches the file and none may, which is unchanged. What the line cost: Shared was the file’s only MayNot producer, so held_by and the second half of way_out went with it, note’s refusal sentence no longer names a reference count, and eight tests across contracts::send and tests/send.rs changed sides.
No type answers may not any more, so NK2501 and NK2502 are built and reached by no program. Stated rather than discovered: the arm stays because the reason for it has not gone away — a type whose implementation does follow user_parallelism has to take the worse of the two settings — and Locked is the candidate. What is lost with it is an end-to-end rendering test of either code, and there is no honest way to write one: a refusal can only come from a type the records name, and they name none. The refusal arm and both its sentences are instead asserted directly in contracts::send’s own tests, and tests/send.rs asserts the new silence at both settings. Part III C.5 gains a Status note saying so and its two examples are rewritten against a hypothetical type, because they were written against Shared.
Shared is still unbuilt, and D6 decides what it will expand to rather than what it does. There is no Rc::new or Arc::new in the emitter and no constructor in the grammar, so a Shared reaches the compiler only as a declared type. What changed today is a verdict, which is real and tested. D3’s own per-build sentence was the same kind of promise.
Where the rule now is: Part I 6.2 (the machinery a second thread may touch, at every setting), Part II 12.2 (the Shared around the counter is not what stops it crossing; the lock is, and nothing decides the lock), Part III 13.3 (std may write a Shared now — what the one-setting pre-lowering still rules out is Locked) and Part III C.5. ADR-005 §5.1’s three-answer table and §5.3 get the narrowing note the layering asks for; no new ADR.
Not decided here. Locked’s representation, which is D3’s second half: RefCell against Mutex differs in what a reentrant access does and in what a panic inside one leaves behind, so it is observable, so it is written and not inferred. Part II 12.2’s lock rules are untouched.
Checked rather than assumed: cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0.
Status notes and two existing ones extended, because a promise a reader cannot tell from a plan is a defect (docs/README.md §1). The eleven constructs the 0.0.9 Found entry reported are now annotated where they are specified - trait (Part I 4.7), the list literal and the list type (I 4.5), macro / quote / struct … with … (II 10.3-10.4), const (II 10.2, so the compile-time half of dual-mode parsing says it has no syntax), select (II 12.4), test / bench / assert (III ch. 14), extern "C" / unsafe (III 15.1), null and String? (I 2.3), ?. (I 3.5), use a::{b, c} (II 10.5) and { line, .. } (I 7.1) - together with the commands: new, test, bench, fmt and explain are not in the binary, which offers build, run and lower-std (III 13.2). Every claim is a probe that was run, one construct per file, and the table is docs/spec-promises.md.fn launch_task(task: @detached fn()), and Nikaia’s type grammar has no function type - which ADR-029 D1 makes deliberate and load-bearing, because a wrapper that took a lambda and passed it on would need effect polymorphism. The subsection now states what is true: which context a lambda is in belongs to the function that takes it, @detached is a property of std’s own entries, and user code cannot declare such a parameter. The heading loses “(The Viral Rule)”, which named a propagation that does not happen; nothing cited it.users.map fn(user) { … } (I 5.3), account.access fn(to) { … } and access_all(…) fn(a, b) { … } (II 12.3) and task::scope fn(s) { … } (II 12.7) are each read as two expressions - a field access or call, then an orphan closure - and a sync marker on a lambda (I 7.2) the same way. assert cond (III 14.2) parses as assert; cond;, so the assertion disappears. A bare throw as a match arm’s body does not parse (I 7.1). And Part III C.3’s “defined so far” meant defined in the specification: nine of its catalogued codes are not emitted, and the note now names which.// to end of line and nothing else - no block comment, no doc comment (Part I ch. 2). A number has no digit separators, radix prefixes or suffixes, so 1_000, 0xFF and 1i64 are each a number beside a name (I 2.2). A newline is an ordinary character in a string literal, so an unterminated one runs to the next " or to end of file (I 2.5).unsafe “remains reserved … rather than being removed from the grammar” - there is no unsafe in the grammar to keep. Part III Appendix B said its capture attributes “are available to library authors” - they are not writable in a .nika file at all, and the ledger has no capture_mode.spawn fn { … } against the parser’s spawn(expr), twelve sites; and throws with a type, where Part I 7.1 states that throws names none and Part I 6.4, NK2601’s help: and Part II 10.2 B write one. Both are the owner’s, and docs/spec-promises.md names the lines.[profile.dev] codegen-backend, which is not a stabilised Cargo key - cargo build refuses the manifest outright, naming the feature. It needed the nightly ADR-001 D1 withdrew. The decision is untouched: Cranelift was already off by default and §4’s measurement still says why. What changed is the price of turning it on, which is now a whole toolchain rather than a profile key.docs/history/std-sysroot.md carried 931 MB for rustc-dev, a figure docs/history/nightly-cost.md §4 had already corrected to 654 MB - 931 MB is lib/rustlib/<triple>, which holds rust-std’s rlibs too. A notes file quoting a number another notes file measured properly is the drift the layering exists to prevent, so it now says which is which.
from(f) is one rule, and touches is not its third case)The question D1 left open is answered: a function type for user code costs one inference rule, not three. D3’s soundness argument — “the lambda runs during the call, so its calls are already counted in the function that writes it” — carries to throws unchanged and does not carry to touches. D3 gains a paragraph saying so in one sentence per column, and docs/history/from-for-throws-and-touches.md is the programs, the ledgers they produced and the reports they printed. No new ADR; nothing about the decision changes.
The argument has a second premise nobody had written down, and it is the one that decides the other two columns. Beside D4’s immediacy, “already counted” only means counted where the contract is read if the analysis reading it asks about a whole function. sync does, so the premise was invisible. contracts::throws does too — it is contracts::sync’s walk with the lattice turned around, over the same visit_expr_blocks, whose Expr::Closure arm is the mechanism D3 describes. contracts::order does not: it asks what one statement reaches, because the rule it serves is about two adjacent statements.
throws needs no key at all, and that is stronger than needing from. fn sortiere(xs) throws { xs.sort_by_key fn { pruefe(a) } … } already records throws = ["?", "LeereZeile"], with LeereZeile coming from the lambda’s body and from nowhere else — the same call over a lambda that cannot fail records only ["?"]. And contracts::throws answers every method call with "?", so a higher-order call is already as pessimistic as the column can be: a throws = "from(f)" read as “adds nothing” could only ever make it less so, which is the direction ADR-010 D1 forbids.
touches must not borrow the spelling, and the reason is a soundness one. A catch handler’s effects are counted into the statement’s touch set (ADR-033 §8.3); a lambda’s are stepped around. Side by side, fs::write(…) catch { println("…") } next to a println is refused as “fs::write + println / println — both reach stdout”, naming the handler’s write as a callee of the first statement, where xs.sort_by_key fn { println("…") … } next to a println is refused for ignorance about sort_by_key and mentions stdout nowhere. So “adds nothing” there would buy an overlap on an incomplete touch set, and the two lines of output would interleave differently from run to run: ADR-033 D1 broken by an effect nobody counted — the same shape as the stdout/stderr mistake §6 caught and the catch hole §8.3 closed. A hole here changes what a program prints, not how fast it prints it.
What touches would want instead is the opposite instruction. Not “this call adds nothing” but add what the lambda reaches, and everything where the lambda cannot be read — the polarity walk_block already gives a handler. One word meaning “skip, it is counted elsewhere” in one column and “descend, nothing else counts it” in another is two rules wearing one name, which is what contracts::touch::KINDS’ closed list exists to prevent.
The refusal that keeps it shut was a _ arm and is now a named one. contracts::order’s walk has an Expr::Closure arm with the reasoning written where the next person will be standing. Same polarity, better message (“a lambda, whose body this analysis does not read”), and it matters because a trailing lambda is today refused two or three times over by limits the records call liftable — a missing touches line, the non-literal-argument rule (ADR-033 D9’s table: “an implementation limit, not a language question”), and a missing crosses line (ADR-005 §1 Group B). Close all three and the lambda arm is the only thing left. The same file already descends into a closure in names_in, for data dependencies: the names counted and the effects did not, and nothing said why.
A stale claim in crates/nikaia-std/std.contracts is corrected. Its header said a higher-order method carries no sync because “a ledger cannot say ‘it depends’ yet: sync = "from(f)" is the shape that would, and it is not decided (ADR-027 §7)” — thirty lines above four entries that say exactly sync = "from(f)". The comment predated ADR-029 D3. Both halves of it now say what is true, and the touches note beside it says why the same shape is not available there.
Three tests hold the answer. a_throwing_lambda_is_already_in_the_enclosing_functions_error_set (tests/contracts.rs) pins the error set, and a_lambdas_own_effects_are_not_in_the_statements_touch_set (tests/ordering.rs) pins the touch set against std’s ledger and against one where Vec::sort_by_key claims touches = [] outright — the permissive entry a from-style reading would amount to. sync’s half was already pinned by a_higher_order_method_hands_on_what_its_lambda_does and is re-run rather than rewritten.
Checked rather than assumed: cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0. No function types were added to the grammar; fn f(g: fn()) is still a parse error, which is D1 and is the owner’s decision to make.
contracts::throws does not use ADR-028’s method resolution, where contracts::sync does, so the ADR-031 chain HashMap[&str, Stats] → fn(&$V) → a.add(v) → Stats::add yields throws = ["?"] rather than ["ZuVoll"] — fail-closed, and one argument away from being exact. (2) An explicit call that can fail inside a function that does not declare throws is not reported by Nikaia at all: NK2601 and NK2701 are both about implicit calls (ADR-025 D1), the ledger records the caller as unable to fail, and the emitted Rust is what rustc rejects. Neither has anything to do with lambdas.
Rc or Arc can be chosen per value)An experiment, and it decides nothing. ADR-037 D3 expands Shared from user_parallelism — no gives Rc, yes gives Arc, per build — and says of itself that whether the choice could be made per value “is a real question and is not answered here”. It was tried, measured and prototyped. The question is still open and the decision is the owner’s; what exists now is a number, three worked cases and a list of what the analysis cannot decide, all in docs/history/rc-or-arc.md. D3 gains a parenthetical pointing at it and keeps its wording.
Rc and Arc differ in nothing a program can observe except speed and Send-ness, which is the gate the experiment had to pass first: D3’s reason for Shared being written by hand is that sharing changes when a value is cleaned up, and if that distinguished the two counts the language’s own rule would forbid inferring between them. It does not. Six assertions in benches/refcount/ run on every cargo test: the handle is the same size, the owner count is the same at every step, the value is cleaned up at the same point (asserted against a written log), a weak handle goes stale at the same point, and exclusive access and unwrapping succeed and fail alike. Two differences are real and named rather than hidden — the thread a destructor runs on (which can only differ for a value that crosses, where the choice is forced anyway) and the count-overflow abort threshold (usize::MAX against isize::MAX, needing 2⁶³ live handles).
The atomic costs +9 ns per clone-and-drop pair, and it is a fixed cost rather than a proportional one. Intel Xeon @ 2.80 GHz, 4 vCPU, 15 GB, Linux 6.18.44, rustc 1.94.1 stable, --release, nine repeats of 50 000 000 operations, load average printed beside every table. clone+drop ×3.40 (+9.028 ns, sd 0.043 on the atomic row); a read through the handle with no clone ×0.99 — the control, which ties and bounds every other difference from below; the same clone and drop with ~70 ns of other work between them ×1.01 (+0.864 ns), the atomic hiding under independent instructions. The whole table was taken twice within the hour and every row agrees to within 1 %, except the noisiest at 7 %. Per docs/history/runtime-cost.md §6.3 only the ratios and the signs travel: absolutes on a shared VM moved 1.4–1.9× from one day to the next.
The expensive atomic is the one no inference can avoid. Four threads cloning and dropping one count cost ×17.19 (+56.9 ns an operation) against four threads with a handle each — the cache line moving between cores, not the instruction. That row is the least reproducible here, at ×11.33 in a third run on a busier box, so what travels is the order of magnitude and not the figure. But a count four threads touch is a value on four threads, which must be atomic; so the case worth 17× is the case with nothing to decide, and the case the inference can decide is worth 9 ns.
A prototype that reports and emits nothing: contracts::sharing and nikaia --sharing. Shared is unbuilt — there is no Rc::new or Arc::new anywhere in the emitter — so nothing was wired into a build. The analysis is real: it runs over the real AST and the real ledgers, it reuses contracts::send’s enumeration of the crossings as its seed list and ADR-029’s walk through a struct’s fields for transitivity, and crates/nikaia/tests/sharing.rs is 14 tests over real .nika sources. The lattice turned out not to be where the difficulty is: a count belongs to the allocation and not to the handle, so the relation between handles is an equivalence rather than an ordering, the analysis is union-find plus one colouring pass, and the least fixpoint of “is atomic” and the greatest fixpoint of “stays plain” compute the same set in one step — unlike sync (ADR-027 D1), whose constraint is conjunctive over a graph that can cycle.
The polarity holds and every undecidable case is named. In doubt, atomic: an inferred Arc where a plain count would do is a cost, an inferred Rc on a value that crosses is a data race (ADR-010 D1). Four cases come out atomic because nothing decides them, and the report marks each as undecided rather than letting it pass for a decision: a Shared in a public signature; one handed to a call nothing describes; one handed to a method nothing describes; one in an argument position no contract covers. A fifth is a hole rather than a case — a Shared inside a type whose fields the ledger does not record is not seen at all.
The one Shared in the repository comes out atomic, where the switch gives it a plain count. examples/fortunes.nika takes db: Shared[postgres::Connection] and hands it to all.fetch(db); nothing written down describes fetch, so the polarity decides. The file’s own comment says the opposite about the build it was written for, and both are right — which makes the inference a regression on the corpus’s only Shared at user_parallelism = no, and a ~9 ns one against a database round trip, so a regression that does not matter either.
And it does not stretch far enough to reach the program it was wanted for. Part II 12.2’s counter is Shared[Locked[i32]]. Arc<T> is Send only where T is Send + Sync, and RefCell is not Sync, so inferring the count per value does not make that counter crossable — Locked would have to be inferred too. There the gate closes: RefCell and Mutex differ in what a reentrant access does (panic against block) and in what a panic inside one leaves behind (poisoning), so that is a different decision from the one Rc against Arc passed. Measured, the pair costs +20.8 ns and ×5.72 an increment, of which about 12 ns is the lock and about 9 ns the count — so the lock is the larger half, and it is the half a program can tell apart.
What it would cost NK25xx, which is not obvious and belongs in front of the decision. Shared is the only type the records make non-crossable (ADR-005 §5.3). A per-value inference makes a Shared that crosses atomic rather than refusing it, so the structural Send check would keep its Undecided arm and lose the MayNot arm it was built for. The two designs are substitutes rather than complements, and a third option the measurement makes cheap — one representation, Shared always Arc — reaches the same place in one line of send.rs with no analysis at all. The price of that one is the 9 ns, and the choice between the three is the owner’s.
Nothing regressed, and it was checked rather than assumed. cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0.
Rc<T> and Arc<T> make a public function two functions — which means a ledger column can record a choice but cannot serve both callers, the way sync = "from(f)" can. Whether a per-function sharing summary suffices instead depends on whether a Nikaia package ships source that this build lowers or a compiled artifact, and Part III 13.3’s manifest says in as many words that how a Nikaia package is resolved is not decided (ADR-002 D1 §5). So D3 has a question under it that another open decision owns.rustc_ast path)Nikaia lowers .nika to Rust source text, and that is the only code generator. Three things that existed to make a second one possible are withdrawn: the pinned nightly toolchain, the Bridge-IR protocol, and the rustc_ast lowering that consumed it. --backend takes interpreter and rust; rust keeps its name, because it says what comes out and because the specification, the cache’s backend dimension and every test already spell it. The decision is the owner’s and is recorded as a withdrawal under docs/README.md §2: the specification and the ADRs are rewritten as though none of the three had been there, one sentence justifies direct lowering (ADR-004 D1), and this entry plus docs/history/withdrawn-one-way-down.md are where everything else goes.
What the three were, and what each was for. Bridge-IR (crates/bridge-ir, 85 lines) was a pure-data intermediate representation — Function, Struct, Let, Expr, Literal, Variable, Call, spans as Range<usize>, no compiler state — and it was the narrow waist: any language frontend would target it, and a rustc change would reach the executor and stop there. The rustc_ast path (crates/rustc-executor) consumed a BridgeModule and built a rustc_ast::Crate in one pass, which rustc_ast_pretty::pprust printed to Rust text and a rustc subprocess compiled; the same crate was to be handed to rustc_interface::run_compiler one day, skipping rustc’s parser entirely. The pinned nightly (bridge-toolchain/rust-toolchain.toml, nightly-2026-01-01 with rustc-dev, reached through scripts/bridge-toolchain.sh) existed for exactly one reason: rustc_ast is rustc_private, and rustc_private needs a nightly and has no stability promise, so the backend that linked it had to be pinned to one snapshot and rebuilt against it in its own CI leg or it would rot.
if, no while, no match, no binary operators — so hello_world.nika reached the backend, let_assignment.nika was refused by name, and all eleven examples/ lower to a grammar! { } invocation that Bridge-IR cannot express. The waist was narrow in the wrong dimension: not disciplined, just empty..nika file becomes a mod in one Rust crate. Worse, the crate the lowering built was print-only — every node NodeId::from_u32(0) and DUMMY_SP, where rustc’s expansion asserts DUMMY_NODE_ID — so the exit was not free to keep open either.-Zpolonius=next changed zero verdicts at +7.11 %. Thirty lowered programs compiled twice: 24 accepted by both, 6 rejected by both with byte-identical codes, 0 verdicts changed, at +7.11 % of the instructions a full debug compile of the largest of them retires. And it was the only reason a -Z flag was ever wanted.librustc_driver-<per-build hash>.so by absolute RUNPATH into the build machine’s rustup home, with the hash in the soname differing per build, so it ran only beside that one nightly installed where it was built. A compiler without the backend links libc/libgcc and is self-contained. That, and not disk space — two megabytes either way — is the measurement that did the most work.Why it is a withdrawal and not a supersession. A supersession leaves a live citation behind, because something still rests on the displaced part; a withdrawal leaves none (docs/README.md §2). After the rewrite nothing cites Bridge-IR, rustc_ast, rustc_private or a nightly, and scripts/check-adr-refs.py is what proves it: it fails on a citation to a Dn that no longer exists, and it is green.
What was deleted. crates/bridge-ir and crates/rustc-executor; nikaia’s rustc-backend feature and its rustc-executor dependency; both #![feature(rustc_private)] — one in crates/nikaia/src/main.rs, one in a root-level check_rustc.rs scratch file — and extern crate rustc_driver; the bridge arm of --backend with both halves of its refusal; the AST-to-Bridge lowering and parse_to_bridge in crates/nikaia/src/parser; crates/nikaia/tests/bridge_backend.rs and crates/nikaia/tests/backend_absent.rs, whose whole subject was the backend and its absence; bridge-toolchain/rust-toolchain.toml and scripts/bridge-toolchain.sh; the bridge_backend CI leg; and findthesource.md, a scratch note whose entire content was a path into the pinned nightly’s rustc_ast sources. _config.yml loses the two exclusions that went with them. The workspace now contains no #![feature(...)] and passes no -Z flag, and rust-toolchain.toml is the only toolchain file, so there is one CI leg.
What stays, renamed. crates/bridge-orchestrator is crates/orchestrator: it is ADR-003 D2’s generic orchestrator, it owns the build cache, the lockfile and the Cargo wrapping, and none of that ever depended on Bridge-IR — only its name did. Its LanguageFrontend trait handed back a BridgeModule and now hands back Rust source text, which is ADR-003 D1 as the interface it always should have been: everything the orchestrator does with a frontend’s answer, it does to bytes.
What the records say now. ADR-001 is retitled and is about the generated parser and the staged bootstrap; its D1 is the toolchain decision with the pin taken out of it — stable, named in one file — and D2, D3, D4 keep their numbers because seven records and the compiler’s own comments cite them. ADR-003 is rewritten around the separation that is real and the text interface that replaces the waist. ADR-004 is one lowering emitting Rust source text, and the text a person reads is the text that is compiled. ADR-005 keeps Group B.2’s situation and answers it with the frontend desugaring in one sentence (D2); its D9 is gone, and so is every mention of the flag in D2, §1, §3 and §5. ADR-006 makes its async_drop argument without comparing against a flag nothing passes. ADR-002 loses its history section and the pin from its price. ADR-021 keeps D9 and its Cranelift measurement — that is a Cargo codegen backend and was never the bridge — and loses D14, whose subject was a backend that could be compiled out and no longer exists. ADR-011 D8, ADR-014 D1 and ADR-038 §4.1 lose their withdrawn halves. No ADR file ended up empty, so none was deleted.
The notes keep every number, and say what became of the question. docs/history/nightly-cost.md and docs/subprocess-cost.md are not gutted: they carry a banner saying their subject was withdrawn without a further measurement and that the numbers below are unrevised, and their closing sections now say what happened to the questions they left open instead of leaving them open. docs/history/std-sysroot.md names its 58 packages as they were named then and notes that the 1.4 GB it weighed the decision against is now 602 MiB with the conclusion unchanged. docs/history/technical_notes.md keeps its rustc_ast findings under a note that nothing links them any more. docs/history/withdrawn-one-way-down.md is the long version.
Nothing regressed, and it was checked rather than assumed. cargo test --workspace green at dev and at release, cargo fmt --all --check clean, cargo clippy --all-targets -- -D warnings clean, scripts/check-adr-refs.py exit 0. There is one toolchain now, so there is one of each to run.
[profile.dev] codegen-backend, which is an unstable Cargo key. With the nightly withdrawn it can no longer be switched on from this repository. D9’s refusal and its measurement are untouched, as asked; whether the switchability claim survives is a decision no record makes.rust-toolchain.toml says channel = "stable", and ADR-001 D5 is the new decision that narrows D1. D1’s premise was two things, -Z flags and rustc_private; neither survived measurement. No -Z flag is passed in any build, and rustc_private lives in exactly two #![feature(…)] attributes, both reachable only through nikaia’s rustc-backend feature. So the whole compiler - the rust backend, the interpreter, projects, the ledger, the type checker - builds, tests and lints on stable, and that is now the leg CI requires to be green.bridge-toolchain/rust-toolchain.toml, and it is a second real toolchain file rather than a channel written into a script for D1’s original reason: rustup’s own file is the only form that states the channel and the components together, and a nightly without rustc-dev is not what ADR-004’s bridge needs. rustup show run in that directory installs exactly what it names - rustup takes the nearest toolchain file, so it wins over the root’s stable - and scripts/bridge-toolchain.sh runs a cargo command on it against the workspace at the root. Nothing else in the repository, CI or the compiler’s own error messages included, spells a nightly.-Zpolonius=next is refused, and Group B.2 goes to the frontend. ADR-005 D9 narrows D2: the flag changes zero verdicts over the thirty programs this repository lowers, costs +7.11 % of a full build, and being a -Z flag would put every user back on a nightly that D5 has just taken them off. What answers Group B.2 instead is the desugaring onto entry-style APIs that D2 itself named as the fallback - affordable because the corpus writes it unprompted: examples/1brc.nika and examples/access-log/src/main.nika both want exactly D2’s situation and both write .entry(k).and_modify fn { … }.or_insert_with fn { … }. What stays true: the classification (Group B, the compiler’s to solve, no user-visible syntax) and the situation’s reality - thirteen lines of Nikaia reach it, and nothing in the repository writes them. D9 is not built.Dn. ADR-006’s async_drop comparison is updated from the other side: a flag that only accepts more correct programs can be declined and picked up unchanged, which is the property the comparison was using.bootstrap_compiler now runs on stable (-p nikaia --no-default-features, build, clippy, test, and the driver over every sample); bridge_backend runs the whole workspace, rustc-executor included, on the pinned nightly through the script. The nightly leg is not droppable: rustc_private has no stability promise and moves with the compiler (ADR-003 §1), so a bridge that is not built is a bridge that rots. Both legs run cargo fmt --all --check, cargo clippy --all-targets -- -D warnings and scripts/check-adr-refs.py; clippy on the nightly leg is new.cargo test -p nikaia --no-default-features on stable: 363 passed, 0 failed, at dev and at release. cargo test --workspace on nightly-2026-01-01: 442 passed, 0 failed, at dev and at release. rustc_private is still confined to crates/rustc-executor (ADR-003 D1).README.md no longer says the executor is “pinned to one exact nightly per release” as if that were the installation requirement, and says what you actually need. Part III Chapter 13 gains the rule - a stable toolchain and nothing more, with the bridge backend as the exception - which it had never stated at all; Part I and Part II turned out to say nothing about the toolchain and are untouched. Part III 13.5’s illustrative ledger header also said toolchain = "nightly-2026-01-01", which contradicted the paragraph two screens down that explains the field records Nikaia’s version and never rustc’s; it reads nikaia 0.1.0, which is what the compiler writes.rust is the default)nikaia --input x.nika with no --backend now lowers through the rust backend, the Stage 0 transpiler. --backend bridge still exists, is still chosen by name, and is still in every build made with the rustc-backend feature. This is the question docs/history/nightly-cost.md §5 and docs/subprocess-cost.md §5 handed on rather than answered, and ADR-004 D4 is where it is answered.rust backend carries all fifteen. A default is what a person gets without asking, and that one answered I cannot to fourteen programs in fifteen - a fact that went unnoticed for as long as it did because, while bridge was the default, it panicked on its first interned symbol and so failed on every input with cargo check staying green.librustc_driver-<per-build hash>.so with an absolute RUNPATH into the build machine’s rustup home, and the hash in the soname differs per build, so a stable install has a differently named file. Built without the feature it links only libc/libgcc and is self-contained. Not disk space, which is two megabytes either way.rustc_ast - and ADR-004 D1’s single lowering stands. The bridge backend is not deprecated: it is the route the language’s own future compilation goes through, and D3’s in-memory exit is the same open option it always was. What changed is which backend a person gets without asking, and whether every installation must carry one.cranelift, which nothing implements; bridge on a build without the feature is the other, and it exists. So one refusal names the feature, the toolchain component and roughly what it weighs, because there is something to do about it, and the other says there is nothing to install - a shared wording would cost a reader the whole difference in the expensive direction. A refusal also lists what this build has rather than what the language names, so it cannot answer one refusal with another.the_backend_changes_the_key), and only the rust lowering is ever stored - the bridge hands its module to rustc-executor and consults no cache at all. So a bare invocation shares the one entry --backend rust already used, which is the same lowering under two spellings, and no bridge entry exists to be served to a rust build or the reverse. backend_default.rs asserts that the two spellings hit one entry rather than forking the store.--backend explicitly, so none of them rested on the default; what rested on it was prose, and bridge_backend.rs’s header is the one that mattered. It said --backend bridge “is the default backend”, which is now history and reads as history - and the file carries more than it used to, not less: while bridge was the default every bare invocation drove that path, and now only these three tests do. A backend nobody exercises is how that panic happened the first time.--backend rust emits Rust from the Nikaia AST in crates/nikaia/src/emit and never builds rustc_ast, so D1’s “exactly one place” and D2’s “never a parallel implementation” are claims about the Bridge-IR path. That was already true while bridge was the default; what changed is which of the two a reader meets first.nikaia --input x.nika --overlaps with no --backend, and that command line only now does what the sentence says: --overlaps and --trust are answered by the rust backend, so under the old default they were accepted and silently dropped.
return at the end of a value was being read as that value)return from the end of any block that stood in value position, and in the common case nothing complained. 1 => { return 10 } came out as 1 => { 10 }, so the value became the arm’s instead of the function’s. It had been recorded (docs/history/nightly-cost.md §3.3) as a narrow match bug surfacing as E0308. It was neither. The failure mode depends on whether the two readings happen to agree on a type, not on what the function returns: fn announce(n: i64) { match n { 0 => { return note(n) } … } println("checked") } compiled, ran, and printed checked on the way out - a silently different program, with nothing for rustc to say about it because both readings of the arm are ().bool doing two jobs, and it reached four shapes. tail in crates/nikaia/src/emit/mod.rs meant both “this statement is the block’s value” - which decides the semicolon, and is Part I 3.1 - and “this block’s value is the function’s return value”, which is what licenses writing fn f() -> T { return x } as { x }. Only a function body is the second, and expr() handed true to every block-shaped expression on the way in. So the same line dropped the return at the end of a match arm, an if branch whose value is taken, a catch handler, and a block or seq block used as a value. A loop body and a function’s own tail were never affected and are unchanged. The fix is a three-state Tail - Statement, Value, Return - in place of the bool, with the rewrite allowed only at Return.return the emitter then deleted. ADR-034 D1 refuses to overlap a statement whose catch handler can leave the function, and contracts::order’s diverts counts any return anywhere in that handler. The emitter dropped the one at the handler’s end - so the analysis was reasoning about a control flow the emitted program did not have. It errs safe (it refuses an overlap the program did not need, never the reverse), so no schedule was wrong; but the two halves of the compiler disagreed about what the handler did and only one of them was right. They now agree, and a test says so.crates/nikaia/tests/returns.rs lowers each shape, compiles it as Rust, runs it and compares the output against what the source means - because a test that only compiled the lowering would have passed, and a text comparison would only have asserted that the emitter agrees with itself. One shape’s verdict changes: let x = { return 1 } used to compile, because the lowering had quietly turned it into let x = { 1 }, and is now an honest rustc complaint about a binding with no value. Nothing in the corpus writes it - cargo test --workspace on the pinned nightly and cargo test -p nikaia --no-default-features on stable are green at dev and release, and tests/errors/EXPECTED.txt does not move. What does change is examples/json/src/main.nika’s longest, whose four arms each end in a return and now say so; it prints what it printed.return anywhere in a handler leaves the function. Both were right and the compiler disagreed with them, which docs/README.md §1 calls a bug in the compiler. Part I 3.1 was silent, though: it said what a block’s last line is worth and never what a return written there means, so the rule now has one sentence in the specification where it belongs.key: &str that inserts into a HashMap[&str, …] field is refused with E0621 before any flow analysis. Stage 0 spells a view’s lifetime by position (ADR-011 D6), and a bare &str parameter is two things at once: one that will be stored needs &'a str, one that will only be inspected must not have it - checked, not assumed, since 'a there is refused with E0515 for a &mut self method whose caller builds the probe locally, and the same program compiles with the lifetime elided. Choosing per view is ADR-008 D2’s tether lattice, which that record says is not built. §3.3.2 states the two open questions. Separately: E0621 is not among the codes ADR-005 D7 enumerates, and its text (“add explicit lifetime 'a to the type of key”) names something ADR-008 D1 says the author never writes - D7’s own E0277 failure at a second code.
.nika, and what writing it found)editors/vscode/syntaxes/nikaia.tmLanguage.json is one file that every current highlighter reads - VS Code, Sublime, Zed, linguist, bat, Shiki and anything else on vscode-textmate - with a minimal VS Code extension (package.json, language-configuration.json) around it and a short editors/README.md. It is a tool: no ADR, no specification change, per docs/README.md’s layering rule. Maintained by hand against both authorities and deliberately not generated from the parser: crates/nikaia/src/parser/mod.rs is a PEG with commit points, actions and recursion, and TextMate is a flat list of regular expressions with no stack - half the language cannot be expressed there at all, so a generator would have had to invent the approximation anyway.f"…" holes as real expressions against "…" whose braces are braces (ADR-035); the ; config zone inside a call and inside a signature, against a statement’s ;; ...args: Self::dsl (ADR-007 D5); throws / throw / catch / ?? as the error syntax rather than a Result; sync on either side of the return type; fn { … } as the one lambda form, trailing and chainable; seq { … } as a keyword only where a block follows it, because examples/k-nucleotide/src/main.nika uses seq as a variable; Vec[i64] in square brackets; @frame(boundary: "\n", unchecked) with unchecked scoped as the assertion it is.dsl body is foreign syntax and gets no Nikaia colouring. html bodies get a small HTML written into this grammar rather than delegated to text.html.basic - delegating loses the holes, because once an HTML grammar enters a tag its patterns are the only ones active and <for r in :rows> and class="{r.shade}" both put Nikaia inside one. SQL targets are marked meta.embedded.sql.nika; every other target gets :name parameters and nothing else. {…} holes are enabled for html alone: a template’s holes need no f because the construct is the mark, but that is the template grammar’s property and in SQL a brace is a brace.editors/test/scope-test.mjs drives the grammar through vscode-textmate itself: 143 scope assertions, plus every .nika file in the repository - 55 files, 10 311 non-whitespace tokens, 676 (6.6 %) with no scope beyond source.nika, every one of them a plain local or field name plus three deliberate oddities in tests/errors/ (G1’s two smart quotes, G2’s café). That figure is measured with the catch-all identifier rule removed; with it the number is 3 of 10 314 (0.03 %) and says nothing, which is why both are reported. The test also checks the rule stack each file ends on: all 49 well-formed files return to the root scope, and the six that do not are the unterminated ones in tests/errors/.f"{emphasised(\"Ada\")}" the \" inside the hole opened a string that never closed and the rest of the file was coloured as one - exactly the failure a hand-read would have missed. The hole’s escaping is the enclosing literal’s, which interpolation() in crates/nikaia/src/emit/mod.rs undoes before parsing; the grammar now has a rule for it and a case asserting it. A second bug was found the same way: a lookbehind excluding a word character in front of a format spec’s colon lost f"{s.min as f64 / 10.0:.1}", which is 1BRC’s own line.editors/README.md says which four lines it would be. editors/ is added to _config.yml’s exclude, for the same reason scripts/ is on it.```nika block on the published documentation looks exactly as it did.crates/nikaia/src/parser/mod.rs rule by rule. Eleven entries’ worth of construct that the specification defines have no rule in the parser at all (the table is in editors/README.md): trait Name { … } (Part I 4.7 - impl Summarize for User parses, the trait it implements cannot be declared); the list literal [1, 2, 3] (Part I 4.5, already recorded in docs/error-corpus.md as the reason tests/errors/A4.nika reports expected expression at the [); macro / quote / struct … with … (Part II 10.3-10.4); const (Part II 10.2, which is what makes a dsl … from … run at compile time, so the compile-time half of dual-mode parsing has no syntax); select (Part II 12.4); test / bench / assert (Part III 14); extern "C" / unsafe (Part III 15.1); null and a nullable type String? (Part I 2.3 - while ??, the operator for handling null, is implemented); ?. (Part I 3.5); use a::{b, c} (Part II 10.5); and { line, .. } in a match pattern (Part I 7.1, so a match over an error type cannot bind some fields and ignore the rest).spawn: the specification writes spawn fn { … } in five places including NK2101’s own help: text, and the parser is "spawn" "(" expr ")" - and spawn fn { … } does not fail, it parses as a variable spawn followed by a lambda, so the program means something else. (2) throws with a type: Part I 7.1 states that throws names no types and the parser agrees, but Part I 6.4 writes fn cleanup(&mut self) throws IoError and so does NK2601’s help text - the specification contradicts itself, and the parser follows the sentence rather than the examples. (3) A string literal spans lines: STR_CHAR’s any matches a newline, so an unterminated literal runs to the next quote or to end of file; the specification says nothing either way, and tests/errors/F1.nika is that case."//" until(line_ending) and nothing else - no block comment, no doc comment, and Part I never says. And a numeric literal is digit1 or digit1 "." digit1 EXPONENT?: no digit separators, no radix prefixes, no suffixes, in either the specification or the parser - so 1_000, 0xFF and 1i64 are each something other than a number.std through a sysroot, and stops building the compiler twice)nikaia-std, clap, anstream, toml, sha2, bridge-ir, bridge-orchestrator and nikaia itself among them. The cause was ADR-014 D1’s when: std lowered its own .nika files in a build script, so it had the compiler as a build-dependency, so Cargo compiled the compiler library again inside the project’s target/ - while the installed compiler was running and doing that very lowering. A duplicate build, per project, for ever, and 29 s cold for a program that prints one line.std ships as sources with its Nikaia half pre-lowered, and the measurement closes. Re-measured where the fix was built: 103 packages to 46, the 58 to 1, 79 units compiled to 38, 25.8 s to 16.0 s. The 58 reproduced exactly on a second machine five months of version drift later, because it is a property of a dependency edge and not of a registry. The 1 that remains is version_check, an honest build script of a crate std itself depends on; nikaia, clap and sha2 are asserted absent by a test rather than by reading a lockfile once. crates/nikaia-std/build.rs and its [build-dependencies] are gone, and a test fails if either comes back.nikaia binary and never by linking the compiler as a library. nikaia lower-std is that command. The linkage was the defect, not an implementation detail of it - so one artifact serves a binary install (compile the .rs) and a from-source one (re-lower if you want to), which is the point: the alternative is two release pipelines that can disagree.std: 1.65 s and one crate compiled, against 16.0 s with the cache switched off. The key is the one ADR-021 already has - the compiler’s own fingerprint (D3) and the toolchain - plus the target and the codegen flags, named in Key::sysroot beside Key::build for D7’s reason. Codegen is in because hardware optimisation is the one axis where std matters more than the compiler: std’s code runs inside every user program, where the compiler’s own speed only costs build time. So -C target-cpu=native is a dimension that coexists rather than a reason to rebuild the other entry. Asserted by the rlib’s modification time, not by an absent “Compiling” line, which would only say Cargo was quiet.user_parallelism, because the switch reaches std as a runtime value - UserCode::Sequential handed to rt::start in the generated fn main - and never as a cfg or a feature; verified, and it still holds (nikaia-std declares no features, and every cfg in it is target_os). ordering and the lowering backend, because they decide what a .nika file lowers to and std’s Rust is not lowered by a project’s build at all. And the content hash of the std tree, because what the key names is a directory and Cargo’s own fingerprinting already owns what is stale inside it: hashing the tree too would start a fresh directory for every edit to src/fs.rs and rebuild everything under it (38 units, 16 s) to guard against a staleness that cannot occur - D7’s “too much” direction.std’s Nikaia half was lowered at Build::default(), hardcoded, so always at user_parallelism = no whatever the consuming program was built at - and nobody had decided that. It is decided now, as a constraint: lowered once, at one setting, sound only while nothing switch-sensitive appears in those files. Shared is exactly what would break it (ADR-037 D3 makes it Rc at no and Arc at yes, and that question is still open). Every .nika file in std is lowered at both settings and the bytes compared, so the day one arrives it is a red build rather than a std that cannot cross a thread inside a program that may..rs is committed, and the tie was broken by a constraint rather than by taste. Both ways cost something - a generated file in a reviewer’s diffs, against a release step that must not be forgotten. What decides it is that cargo test --workspace must work from a fresh checkout with no extra steps, and at that moment the only mechanism that could produce a missing .rs in time is the build-dependency this removes. So committing is not the convenient option, it is the only compatible one; and the cost is paid by making the file unable to lie - a test regenerates it with the compiler, compares bytes, and names the command that fixes a drift, the same standard ADR-005 D8 sets for the ledger.std.contracts stays inside the compiler (include_str!), and the compiler answers from that copy and never from the sysroot. ADR-005 D8 makes ledger stability across toolchain versions explicitly not required, so a std that could be paired with a different compiler would let the ledger describe a compiler that is not there. The sysroot’s copy is the source it was built from, and a test requires the two to be equal.NIKAIA_SYSROOT replaces NIKAIA_STD_PATH, named for what it is: the old name said crate directory, and a crate directory can answer only one of the questions this mechanism has. The two runtime versions a generated program may name (winnow-grammar, winnow) are baked into the compiler by its own build.rs instead of read from [workspace.dependencies] at run time - a sysroot is not a Cargo workspace and has nothing above it, and these are the emitter’s versions anyway.std lives in the user’s cache directory and not under target/, because shared-between-projects is the whole of what it is for - so rm -rf target no longer gives a cold build, and ADR-021 §3’s owed eviction policy now has a second store to cover. And two builds sharing an entry serialise on Cargo’s lock on that directory, where two projects with their own target/ did not.nikaia-std/ in it, which is why crates/ is a sysroot and the in-tree flow needs no special case - but nikaia-std/Cargo.toml still spells its shared dependencies { workspace = true }, so a copy of it outside this workspace does not resolve. Method, machine and the full list of 58 in docs/history/std-sysroot.md.
-Zpolonius=next)-Zpolonius=next is passed nowhere, and three records rested on it as though it were. ADR-001 D1 justified the pin with “-Z flags and rustc_private”, ADR-005 §1 Group B listed the Polonius case among what “the compiler solves”, and the ADR index called the pin “the premise Group B.2 rests on when it enables -Zpolonius=next”. Nothing in crates/ has ever put that flag on a command line. The workspace holds exactly two #![feature(…)] attributes and both are rustc_private; the only -Z in the tree was -Zls=root in a test helper, and it is gone. The pin is paid for by one thing: ADR-004’s bridge backend. Corrected in place in all four records, because this is a stale claim and not a new decision..nika file the bootstrap compiler can lower - thirty crates, from examples/ through nikaia-std’s own text.nika to the five tests/errors/ files that get past the parser - compiled twice on the pinned nightly: 24 accepted by both, 6 rejected by both with byte-identical codes, 0 verdicts changed either way. The harness is known to work, because three hand-written controls flip: ADR-005 D2’s own get_or_insert over a HashMap (E0502), the same shape over a Vec (E0502), and a borrow live on one path and dead on the other (E0503) are all accepted with the flag and refused without it.examples/1brc.nika and examples/access-log/src/main.nika both want exactly that situation and both write .entry(k).and_modify fn { … }.or_insert_with fn { … }. D2 named entry-style desugaring as the contingency if the alpha regressed; the corpus has been doing it unprompted all along, because it is the natural way to write it. The shape is reachable from Nikaia - thirteen lines lower to Rust that NLL rejects and Polonius accepts - so this is a real Group B situation that nothing in the repository writes, which is a different statement from “the language cannot produce it”.callgrind, deterministic: a full debug compile of the largest corpus program goes from 1 731 303 418 to 1 854 471 352 Ir, +7.11 % - and +15.1 % of the front end alone on that file. Plus the cost that is not a percentage: -Z is nightly-only, so passing it would move every user’s build onto the pinned nightly, where today only the bridge backend’s own compilation is. D2 stands as decided, with the ordering now explicit: build the frontend desugaring first; the flag earns its 7 % the day a program somebody wrote needs it.rust-toolchain.toml’s four components add over stable-plus-rustfmt-plus-clippy (1 420 MiB / 24.7 s against 602 MiB / 12.3 s) is a build cost, and rustc-dev is 654 MB of it - not the 931 MB that had been quoted, which is lib/rustlib/x86_64-unknown-linux-gnu and includes rust-std. 41 MB of rustc-dev is rustc’s own sources, which nothing here reads.nikaia links librustc_driver-bcf6c5c396c0890f.so with an absolute runpath into whoever built it, and the soname carries a per-build hash - a stable install has a differently named file. So that binary is not relocatable and runs beside exactly one nightly installed at one path. Built --no-default-features it links libc and nothing else.[[bin]] name = "nikaia" had required-features = ["rustc-backend"], so there was no compiler without the bridge - and that was packaging, not a constraint: rustc_executor is named in exactly one place in main.rs. The attribute is now #![cfg_attr(feature = "rustc-backend", feature(rustc_private))], extern crate rustc_driver and the Bridge-IR frontend are behind the same cfg, the required-features line is gone, and cargo build -p nikaia --no-default-features produces a whole compiler on a stable toolchain in 38.0 s. rustc_private stays confined to crates/rustc-executor (ADR-003 D1); no frontend gained any.--backend bridge on such a build says which feature, which toolchain component and roughly what it weighs, and compiles nothing. crates/nikaia/tests/backend_absent.rs asserts that and that the rust backend still lowers, so the first assertion cannot pass on a compiler that refuses everything; bridge_backend.rs is now the feature’s own test and compiles only with it.cargo test -p nikaia --no-default-features on stable is green at dev and at release, which means examples.rs lowered every runnable example at both settings of user_parallelism, compiled the emitted Rust with a stable rustc, ran it and compared what it printed - and one_brc.rs, project.rs, modules.rs, ledger_determinism.rs and foreign_runtime.rs did the same for theirs. Clippy is clean there too. A second CI leg runs exactly that, so the claim cannot rot quietly.-Z, and it was load-bearing for nothing. crates/nikaia/tests/common/mod.rs asked rustc -Zls=root to print an rlib’s dependency list, so the suite needed the pinned nightly even where nothing it compiled did - and a stable run was impossible. The same question is answered without a flag now: a dependency’s extra-filename is a plain string inside the metadata, so the one copy that belongs is the one candidate on disk whose filename hash occurs in the anchor’s bytes, and the helper refuses to guess if that is not exactly one.bridge is still the default everywhere, so a stable installation is one where nikaia --input x.nika refuses by name and the user says --backend rust. Whether the default should resolve per build belongs to ADR-004 and ADR-021 D9 and nothing was changed to imply an answer.return <expr> inside a match arm is dropped, emitting the value as the arm’s value instead (E0308 here, a silently different program in a function returning nothing - a bare return in an arm is kept); and a method taking key: &str that inserts into a HashMap[&str, …] field lowers with the key’s lifetime elided, so E0621 arrives before the borrow checker’s flow analysis is ever reached.seq is the keyword, settled. ADR-033 D7 wrote it down as a placeholder for “a word chosen later”, and Part I 8.1.1 warned that a program written today might have to be renamed. It is kept: a second round of naming a block almost nobody writes buys nothing, and it already reads as “in this order, whatever you think”. The warning is gone from the specification.nikaia-runtime.toml in the working directory, with NIKAIA_RUNTIME_CONFIG naming one outright for a binary serving several machines. ADR-038 D5 named the four settings and said “read at startup” and nothing else, so the implementation had chosen a name under protest; it is decided now, with the keys spelled as nikaia.toml’s are because a person who has written one of the two files has already learned the convention.stdout and stderr are compared as one destination, confirmed. They are two handles and one file the moment anybody types 2>&1, which is why println / eprintln / println interleaved differently on every run of a redirected program. seq is what a program writes where the permissive reading would have been wanted.Send check, NK2501 and NK2502)crates/. ADR-005 §1 Group B decides it, Part III C.3 reserved NK25xx for it, and ADR-037 §3 and ADR-038 D7 both rely on it. The only Send in the workspace was a trait bound inside nikaia_std::task::both. It is built now - crates/nikaia/src/contracts/send.rs walks a type structurally and transitively, through a container’s arguments and through a struct’s fields as the ledger records them, and terminates on a type that holds itself.Shared, and that ordering is the whole point. Today’s silence costs nothing only because Nikaia has no value that may not cross a thread: Shared is unbuilt, so every value a program can construct is crossable, and rustc has had nothing to refuse. Shared at user_parallelism = no creates the first one, and a program handing it to a foreign call would then be accepted at yes and refused at no - the exact asymmetry Group B, NK25xx and ADR-037 §3 were all written to prevent - with a raw E0277 as the only signal.no nothing the program wrote runs concurrently, so a crossing into a task does not happen in that build and NK2501 is a lint there rather than a refusal - which is what Part III C.3 asked for, and refusing would have refused a program that compiles. NK2502 is an error at both, because a Rust dependency’s own threads are not bounded by a switch about your code (ADR-037 D2’s load-bearing “user”). A test runs one program at both settings and compares the verdict rather than the exit code.task::both of statement overlapping) is simply not taken, which costs speed and never a program - ADR-033 D4’s own polarity; a crossing the program wrote is refused where it can be decided and handed to rustc where it cannot, and rustc’s refusal now arrives against the .nika line.task::both (the result of each overlapped operation crosses back, and the captures are empty by construction today); spawn (NK2501, reported ahead of the emitter, which cannot lower one yet); a value handed to a call this compiler cannot see the end of (NK2502, ADR-038 D7’s foreign runtime). A scope’s tasks are not covered: task::scope has no syntax in the grammar and no AST node, so there is nothing to check.crosses = true is a new ledger key on a type, and it had to be. A Rust type records no fields, so the structural walk answers “nobody said” about cli::Args - and that is not permission, so the ten overlapping pairs ADR-033 §8.1 bought with a touches line would have gone again. The key is a promise about a body this compiler does not read, written in std.contracts and reviewed like code, exactly as sync = true on a Rust function is. Its absence stays “nobody said” and never “it may not”. Measured: that one line is the difference between the corpus’s overlapping surviving and not.examples/foreign-runtime/smuggled puts a plain reference count on a foreign worker thread through an unsafe impl Send that lies, and no frontend check can catch it: where Nikaia can see the value it is crossable by declaration.Shared may not cross a thread at all - because the verdict may not depend on the build - and Part II 12.2’s shared counter is the program that wants to. That has to be answered before or with Shared. It is recorded in ADR-005 §5.3 and ADR-037 §3 and nothing was invented for it.nikaia build had no diagnostic interception at all, so every backend error reached the user as Rust about target/nikaia/gen/….rs - a file the build wrote and the author has never read. Part III C.1’s Iron Rule calls that a bug in this compiler, and the mapping that fixes it had worked since ADR-012: it was only reachable through --explain, which reads rustc --error-format=json on standard input. The channel was what was missing. cargo --message-format=json is it - every rustc diagnostic arrives on stdout as one JSON object per line, and diagnostics::translate now reads them there.E0277 joins the classes ADR-005 D7 enumerates as translated. All seven it named - E0382, E0499, E0502, E0505, E0506, E0597, E0716 - are borrow, ownership or lifetime errors, and a trait bound is how Send, sync and every future marker rule surfaces from the backend. The list was short by one class, and the ADR-038 D7 experiment is what found it.xs does not live long enough” is already a sentence about the Nikaia source with only its place wrong. That argument fails for a trait bound, whose every noun is a Rust type the author did not write. So the caret lands in the .nika file and the sentence is still Rust’s; the gap is closed from the other end, by the frontend refusing in Nikaia words what it can decide. Rewriting a trait-bound message is a second question and is named as open in ADR-005 D7.nikaia run is now a build and then a run, because --message-format=json can only be read where stdout is not the program’s. The build’s diagnostics are read; the run inherits stdio as before and is a no-op rebuild - the same shape the lowering already had, where the work happens once and the decision is made where it can be reported. Cargo’s stderr is still inherited, so its progress and anything it says that is not a rustc diagnostic - a manifest it cannot parse, a dependency it cannot resolve - still reaches the user; a build that failed in silence is the one outcome worse than an untranslated error..nika files its entry reaches, joined into one Rust file; the map has carried the unit index since that was true, and until the project build started reading these messages a diagnostic about the third module would have been printed against the first module’s text.
ordering = "effects" means something at user_parallelism = no now. A pair of adjacent std file reads lowers onto nikaia_std::task::read_pair - two operations the kernel performs while the program is suspended at both of them, with std doing the waiting. ADR-033 §8.2b made effects degrade to strict there, on a reason that was correct at the time: overlapping meant two user closures on two threads, and at no nothing you wrote may run concurrently (ADR-037 D2). ADR-038 D3 and D4 built a vehicle that carries no code of yours at all, so what degrades at no is task::both and not the ordering. §8.2b’s category distinction - whether two operations may overlap is a question about the program, whether a vehicle exists is a question about the build - is what the new decision rests on; only its conclusion is narrowed, and it is narrowed by a new numbered decision rather than by an edit.Build::overlaps_user_code() answered may two pieces of the program’s own code run at once (yes only, and it still gates task::both) and may two operations be in flight at once (both settings) with one boolean. They are two methods with two names now, and contracts::order gained the half that is about the program - which vehicle a pair would need - while still seeing no build switch. The word user in user_parallelism is what the whole change turns on, and this is the first place it pays for itself.std surface: a .nika program lowered twice from one source, 200 000 pairs a run, nine repeats, both of ADR-038 D3’s mechanisms. −0.87 µs a pair (sd 0.14) on the completion path against +112 µs for task::both on the same box the same hour - which is why a pair of reads gets this vehicle at user_parallelism = yes as well. Method, machine and load average in docs/history/runtime-cost.md §6; benches/overlap/lowered.sh is the script.join - seq at +112 µs where §4.3 records +59 µs, and both - seq at −0.98 µs where it records −0.25 µs - every figure 1.4 to 1.9 times larger, the baseline included, on a shared virtual machine a day later. The end-to-end number agrees with that same-day control, so there is no cost hiding in the emitter; what there is instead is a bound on how far an absolute microsecond from these notes travels. Every conclusion §4.3 drew reproduced exactly, because each one is a sign, a flatness or a ratio rather than an absolute.io_uring, the pair is performed in the order it was written, and that is a decision rather than a fallback. The blocking path can overlap a pair and charges ~38 µs to do it (~54 µs on the box above) - the fixed per-pair tax ADR-033 §8.2 measured and called a pessimisation, which cannot be predicted from the source because the payload is not in the source. So an overlap the compiler chose is taken only where it is free: measured at −0.05 µs a pair, against +54 µs for fs::read_both through the same mechanism. fs::read_both is left exactly as it was - it is what a program asks for, and a program may know the payload. The cost is named: at user_parallelism = yes on a machine with no completion queue, two reads of a quarter megabyte or more lose an overlap they had.--overlaps answers per pair. One note at the top saying which setting made the report hypothetical was true while the answer was the same for every pair; at no it is not, and a report that cannot say why not is the trap D9’s refusals exist to keep open to a question. A pair the program allows and the build cannot carry now prints would with the switch that decided it, beside the together of one it can.examples/, the same seven refusal reasons, the same counts - and now the same counts at both settings, because no pair in those eleven files is a program-level overlap at all. The zero was never the build’s doing, which is precisely what §8.2b’s degradation made impossible to see from the report. The one pair in the repository that moves is the control of examples/foreign-runtime/overlaps.nika, which ADR-038 D7’s experiment put there for this purpose and which now overlaps at no with no task::both anywhere in the program.no overlap their first pair and leave the third where it stands; and a pair where either half is a method call, for §8.1’s standing reason. One thing found while measuring and left for the owner: a Nikaia project has exactly one profile, so a lowered program always runs with debug_assertions on, where every number in these benches is --release.sync survived a call nothing could resolve)sync a source asserted was kept even where the body called something no ledger knows, and the ledger that ships recorded sync = true for it. ADR-027 D2 makes the inference conservative in the restrictive direction and D4 says an assertion is never overwritten by the inference - so the two together left the assertion standing, and a consumer’s par_iter body would have believed it. sync gates access, access_all, par_iter, a scope’s tasks and the panic hook, so this is the gate on every safe concurrency primitive Chapter 12 has.NK2202 wants a caret on the call and the type checker answers per function. But Part III C.2 reports this checker at statement granularity already, which is a position - so the caret was available all along, and the polarity ADR-010 D1 exists to protect was being paid for it.sync function calling into a foreign crate is exactly this shape, and a foreign crate has no contract by definition.nikaia.lock now carries [dependencies], which ADR-021 D2 has asked for since it was written. The versions only started existing an hour earlier: since ADR-002 D1 a project build generates a Cargo.toml and Cargo resolves a real Cargo.lock beside it under target/, and nothing carried the answer across. Until it did, the one file whose job is to answer does this build the same thing for you as for me? answered it only about the Nikaia half - source hashes, toolchain and compiler fingerprint, and not a word about which regex was linked.[[package]] entries after cargo returns - which is D4’s declaration-versus-record distinction in the small: nikaia.toml declares version = "1.5" and the lock records the 1.13.1 that turned into. The transitive crates are in it too, because a crate nobody wrote down that resolved differently on two machines is still a different build. One entry per name holding a list of versions, because two majors of one crate in one graph is ordinary and a record that keeps one of them is not the whole answer.Key::build for symmetry.BTreeMap/BTreeSet makes that unrepresentable, and two nikaia build processes over two copies of one project are compared byte for byte, because a hash map’s iteration order is randomised per process and a same-process comparison passes over exactly the slip that is easiest to make. Nothing absolute goes in. Nothing is written when the versions are unchanged, so a committed file does not turn up modified after a build that changed nothing about it, and a failure to write it warns and leaves a finished build alone (D12).rustc-executor passed rustc no --edition, while the crate had been interned and printed for 2021 - the edition the emitter writes and the edition every test of the rust backend compiles at. Found while measuring ADR-004 D3 and reported rather than changed, because changing it changes what is compiled. It is a mismatch and not a choice: D2 makes the printed text a view of what is compiled, and this was two statements of one fact with one of them missing. Both now read a single constant.sha256 01455af3… and a474e629… on hello_world), and crates/nikaia/tests/bridge_backend.rs compiles the same printed text at 2015 and at 2021 and compares what the two binaries print. That last assertion is the useful one going forward: nothing Bridge-IR can express tells the two editions apart today, and the day something can, the test fails and names the reason instead of the mismatch quietly returning.--locked for the lock. D10 fixes what it would have to mean - do not re-resolve - and nothing passes it to Cargo; today the flag is the ledger’s regenerate-and-compare. Recording the resolved versions needed no answer to that, and none was invented.
hyper)hyper is the cheapest test of whether ADR-002 D1’s crates.io promise and this record can hold at the same time”. They hold: examples/foreign-runtime/serve starts a hyper server, serves one request and prints served: GET /hello on tokio-rt-worker. Twenty-six crates resolve from crates.io, the whole graph compiles in eleven seconds, and NIKAIA_WRAPPER_TRACE over the build contains exactly one line - the project’s own crate. A proc-macro dependency in the graph (tokio-macros, and syn with it) is a non-event, which is worth recording because ADR-002 D2 is about Nikaia’s own metaprogramming and says nothing about a user’s.extern block. hyper’s cannot be spelled - async, generics over futures, trait bounds, turbofish - so the shim is a narrowing of a generic Rust API down to i64 and String, the same work a C binding does for a C++ library. That has a consequence D7 does not mention: the signature that decides what crosses into a foreign thread is hand-written Rust no Nikaia analysis reads.Send check does not exist. It is decided in ADR-005 §1 Group B, reserved as NK25xx in Part III C.3, and relied on by ADR-037 §3 and ADR-038 D7 - and there is no such analysis anywhere in crates/. Nothing is unsound yet, for a reason that is not the rule: Shared is unbuilt, so every value Nikaia can construct is Send, the emitted Rust contains no unsafe, and nikaia_std::task::both carries Send on both closures. What refuses an illegal crossing today is rustc. It becomes unsound the day Shared lands with nothing more than this in front of it - and the first symptom will be the asymmetry Group B exists to prevent, a program accepted at user_parallelism = yes and refused at no.rustc error, which Part III C.1 calls a bug in this compiler. error[E0277]: Rc<String> cannot be sent between threads safely, spanned against target/nikaia/gen/…rs - a file the build wrote and the author has never read - in Rust’s vocabulary, with no NK code and no way out. Two details: E0277 is not among the classes ADR-005 D7 enumerates as having translations (all seven of those are borrow, ownership or lifetime errors), and the machinery that would place it correctly already works - fed the same rustc JSON by hand, diagnostics::translate puts it on main.nika:14:5 under the right statement. It is only reachable through --explain; nikaia build shells out to cargo and hands its stderr to the terminal untouched.examples/foreign-runtime/smuggled calls a foreign function whose author wrote unsafe impl Send for a type that is not, and it builds, runs, and reads an Rc refcount on a tokio worker. A structural check in the frontend would not have caught this one either - where Nikaia can see the value it is Send by declaration. That is ADR-033 D4’s “reached through unsafe” arriving at a second question.cargo test. A foreign call has no ledger entry, so touches is absent, so ADR-033 D4 makes it reach everything: --overlaps over four pairs keeps every pair with a foreign call in it in order, with the absent touches as the stated reason, while the control pair of two file reads still overlaps - and the emitted Rust carries exactly one task::both, inside the control. The test needs neither Cargo nor the network. The three that drive Cargo are #[ignore]d, because the first run fetches from crates.io, and are not in CI.cleanup() is never emitted at all: ADR-006 D1 inserts it at the exit of a Nikaia block, and a value handed to a foreign function leaves Nikaia’s control flow at that call, so RAII degrades silently to the synchronous fallback. And ADR-006 D5’s drain cannot report it: a value owned by a foreign task is neither a parked cleanup nor a detached task, so the deadline is not exceeded, NK2603 has nothing to name, and the program exits 0. Also measured while checking it: the sync check deliberately shrugs at a call it cannot resolve (Reached::Opaque => None, for a diagnostic-span reason), so a function declared sync that calls into hyper compiles today and is recorded as sync = true - a Cleanup rule built on the same walk would inherit that hole.unsafe impl Send demonstration) are in docs/history/foreign-runtime.md, with the four things it leaves for the repository owner to decide - including a sound alternative to refusing a Cleanup crossing that ADR-038 did not consider.
task::both, so the vehicle is unchanged and the next change of vehicle is still a std change.read("a") / read("b") / write("a") has two disjoint adjacent pairs and is not a disjoint run; in a group all three run at once, so a chain of adjacent answers would have overlapped a read of a file with the write of it. Every pair (i, j) with i < j is asked about, and a group is the longest prefix that passes - leaving a statement out of the middle would reorder the ones after it past the one left behind.rayon::join propagates the first closure’s panic when both panic, so nesting to the right surfaces panics in written program order by construction rather than by luck.seq { … } is built (D7): the statements inside keep the order they were written in whatever their touch sets say, and that reaches inward through every block, if and loop written inside it. The block keeps its own place too - the fail-closed answer and the only one D7 decides. It is an Expr variant and not a flag on a block, for the reason LitInterpolated is a variant: a flag is what an analysis walks past, and stopping a reordering is the one thing this construct is for. The keyword is provisional, as D7 says of itself; Part I 8.1.1 now says so where a reader sees it, and the word is still the owner’s to pick.cli::args reaches args read - it was the most-refused callee in examples/ at ten adjacent pairs - and String::new, Vec::new and html::escape reach nothing and now say so. D2’s endpoint(url) and its lock are deliberately not added: neither net::post nor Locked exists, and a vocabulary word with no entry behind it is the speculation ADR-028 D5 forbids. Part II 12.2 carries a Status note saying that an access block is therefore ordered by ignorance rather than by contract. Standard input is the near miss - io::read, io::lines and io::read_to_string are in the ledger with everything but a touch set, and nothing in the corpus reaches the question - and the trap waiting for whoever adds it is written down beside the list: the entry is stdin write, because write means it changes the resource and reading a stream consumes it, so the read/read rule that lets two file reads overlap would be exactly wrong.stdoutt write in a hand-maintained ledger would have bought an overlap rather than failed to buy one - a typo with a semantic reward. The kinds are a closed list now and a word outside it is answered the way D4 answers everything it cannot read: the function reaches everything and keeps its place. A test asserts every kind in std.contracts is one the compiler knows, because that refusal is silent by design.stdout and stderr are one destination under 2>&1. println / eprintln / println was a group of three whose output interleaved differently on every run of a redirected program - D1 broken by a resource whose identity nobody had checked, which is the catch-handler hole of §8.3 in a different disguise. They are compared as one destination now; it costs nothing measurable (console writes are microsecond work, which §8.4 says the overlap cannot pay for whatever carries it) and it buys back the guarantee. The refusal names seq as the way out.examples/, and still zero overlaps. 57 no operation at all (was 50), 32 nothing says what the callee reaches (was 39), 13 ADR-034’s conditional handler, 9 its own control flow, 8 text with code in it, 6 the function’s own value, 2 a written type. The seven pairs that left D4’s row are the four ledger lines doing what D4’s closing sentence says they will, and where they went is the answer to what is left: inward, to the method call beside them - cli::args().nth(1) is an nth this compiler cannot resolve without the type checker.catch-handler fix changed which reason a pair prints - a method call stopped being its own row and became “nothing says what the callee reaches”, a ?? over no call became “performs no operation” - and nobody re-ran the count. Both numbers are in the ADR, because a correction to a measurement belongs beside it.std’s mutating and higher-order methods: Vec::push reaches no resource, and touches = [] on it would rest on the literal-argument restriction holding the line, because this analysis finds a dependency by comparing what a statement binds against what the next mentions - and xs.push(a) twice binds nothing and mentions xs in both. Memory is the borrow checker’s question (D3) and this analysis does not have its answer.
main, and fn main is now the runtime’s. The emitter renames the program’s own entry point to __nikaia_main and writes the fn main Rust runs: rt::start, the program, finish. ADR-038 D4’s reason is that an I/O thread already running costs nothing per operation where one started per pair costs a wake-up per pair - the whole of ADR-033 §8.4’s finding. A generated wrapper rather than a link-time constructor, because ADR-006 D5’s drain has an outcome to report and an .init_array entry has nowhere to put one. Only the plain shapes - fn main() and fn main() throws - are wrapped; a main the wrapper cannot type keeps its own name rather than being guessed at.user_parallelism allows, and the compiler is what says so. At no: the I/O workers, and everything the user wrote on the main thread. At yes: a pool for user code as well. It is written into the generated call because it is a build switch - ADR-038 D5 has four operating settings and this is deliberately not one of them (ADR-037 D2).std surface. nikaia_std::rt is D3: io_uring for fs::read, fs::read_to_string, fs::write and the new fs::read_both; a poller over epoll for a socket’s readiness; and the blocking path on an I/O worker as the fallback. fs::map is untouched - it hands back pages the OS owns and there is no transfer for a completion queue to report. Nothing about any of it reaches a .nika file, which a test now asserts over the whole corpus rather than promising in a comment.io_uring runs on one without it: the probe is IoUring::new answering ENOSYS on an older kernel or EPERM in a sandbox. The only compile-time gate is on the bindings, which do not exist off Linux at all. crates/nikaia/tests/runtime.rs forces one compiled binary down every route - detected, blocking pinned, one worker, two workers, uring pinned - and asserts it prints the same thing and writes the same file each time.Op, a closed enum private to nikaia_std::rt, and no variant carries a closure or a Box<dyn FnOnce>. Outside that module there is no way to name a value the channel accepts, so ADR-037 D2’s promise that nothing the user wrote runs concurrently at no is a property of the code and not a convention review has to keep. A debug_assert closes the other direction: no std I/O entry point is ever on a worker.fs::read returns owned bytes, so std allocates, moves in, and moves out (ADR-005, ADR-008’s tether lattice).nikaia-runtime.toml, read at startup: four settings and no fifth (D5). io-workers (default 1), user-pool (default 0 = as many as the machine has), io-method (default auto, pinnable) and cleanup-deadline (default 30s, 0 disables draining). A key outside the four is a typo and fails, the same rule [build] follows. Pinning uring where there is none is a failure to start, not a silent fallback - the point of pinning is to find out. NIKAIA_RUNTIME_CONFIG names the file for one binary in several deployments. Hand-written reader rather than a TOML crate: nikaia-std is linked into every program this compiler produces, and four key = value lines are not worth a parser in every one of them.cleanup-deadline has moved out of nikaia.toml. How long a program waits at exit is an operating property, and a build-time key cannot be tuned by the operator - who is not the person who compiled it (D5). The manifest still accepts it and prints where it went, because refusing it would fail a manifest somebody wrote to the specification that documented it. Part III gains 13.3b for the new file and 13.3 loses the key.fs::read_both on the pre-started runtime adds nothing per pair. −0.25 µs at 5 bytes a file, −0.48 at 4 KiB, −0.61 at 64 KiB, −0.86 at 256 KiB, each with a spread that straddles zero. The same two reads through task::both add +59.2 µs (sd 1.4), which reproduces §8.4’s 46 µs and is higher for a stated reason: fixed timed a pair with nothing in it, and here the stolen closure’s wake-up sits on the critical path of real work. §8.5’s “none in principle” is “none, measured”.main” is necessary and not sufficient: D4 removes the thread start, and only D3’s completion path removes the thread. ADR-038 §1’s “that pool is the 46 µs” was an argument from the literature and is now a measurement of this repository’s own fallback.std::fs::read reallocates a megabyte to find end-of-file where the ring path does not - and nothing in the conclusion compares them, since every number it rests on is a difference within one mechanism. Method, machine, every repeat and the confound: docs/history/runtime-cost.md, produced by benches/overlap/runtime.sh.fs::read_both exists, is measured, and no lowering uses it: at user_parallelism = no, ordering = "effects" still degrades to strict (ADR-033 §8.2b). §8.5 gave that degradation three reasons - no event loop, no sidecar, no async lowering - and two of them are now gone. Whether a statement pair may be lowered onto a completion-based pair is ADR-033’s question, and answering it inside ADR-038’s delivery would be deciding another record’s business.io-uring 0.7 (Linux-only bindings, no runtime, no executor) and polling 3.11 (epoll/kqueue/IOCP, no task model). Neither brings an opinion about what a program does when I/O is ready, which is what would have re-decided D1 and D3 by the back door - and tokio and hyper are still absent.tokio was never decided, only repeated. It appears in a parenthesis in ADR-018 §3 and as a roadmap checkbox, and three records wait on “the runtime binding” without one of them saying what it is. ADR-038 decides it, with the argument against written down so the question does not come back.hyper is not adopted. A finished server has to be handed I/O it can ask data from; io_uring reports data instead, and bridging the two costs a copy through an intermediate buffer that spends most of what io_uring was for. Adopting one does not postpone the I/O decision - it makes it, for sockets. The positive reason is bigger: an HTTP request is a byte grammar with a frame boundary, which is what ADR-009 exists for, and it is the one real program that exercises ADR-010’s untrusted input and ADR-017’s escaping contract.epoll cannot read a file at all - a regular file is always “ready” and the read blocks in the kernel anyway, which is why every readiness-based runtime serves files from a thread pool, and that pool is ADR-033 §8.4’s 46 µs. So: io_uring for files where the machine has it, readiness for sockets, both behind one std surface so the next change of mechanism is a std change.main. An I/O thread already running costs nothing per operation; one started per pair costs a wake-up per pair. At user_parallelism = no that is all that starts - the I/O thread is one of the compiler’s threads and was never bounded by that switch (ADR-037 D2).auto and pinnable - the shape --ordering strict already has. cleanup-deadline moves there from nikaia.toml, where Part III 13.3 had put it: how long a program waits at exit is an operating property.std::http promised “production-ready HTTP/1.1 and HTTP/2” with no status note, which is the kind of promise docs/README.md calls a defect. It carries one now.user_parallelism = no, Shared is Rc), and a Cleanup may not be owned by a foreign task (ADR-006 D3 parks it with our runtime, and a foreign shutdown will not drain that queue). What needs no rule: a foreign crate has no touches, so it reaches everything and orders against everything - fail-closed polarity means interop cannot silently break ADR-033, only make programs slower.catch handler’s effects were not counted)catch at the call it guards, so the handler’s own effects never reached the statement’s touch set - D1’s guarantee broken by an effect nobody counted. A handler is code that runs, so what it reaches counts; where it cannot be read, D4 answers as it answers everywhere else.catch { "".to_string() } is a method call and which ledger entry a method call is needs the type checker (ADR-028). A weaker question does not: if every ::to_string in the ledger reaches nothing, then "".to_string() reaches nothing whatever its receiver is. An over-approximation over the candidates, in D4’s direction, and it needs no type checker.println had no ledger entry at all. ADR-033 D2’s own table names it (println(x) touches stdout write) and D6 rests on it - two printlns stay ordered because both touch stdout, not by a special case. Unentered, they were ordered by ignorance: the right answer for the wrong reason, and not the right answer any more as soon as something else is in the pair. print, println, eprint, eprintln and str::to_string are entered now.println, and a handler the walk cannot read.--overlaps weighed 127 pairs across examples/ and refused all 127, and 101 of those refusals were the analysis’s own narrowness - “one of them is not a let of a single call”, decided before any touches set was consulted. ADR-033 §8.3 says widening the shapes comes before any conclusion about whether ordering = "effects" earns being the default, because a zero produced that way measures the analysis and not the corpus. It is widened.println(x), fs::write(p, d), out.push(y) - an operation the ledger can account for that binds nothing. A pair where neither half binds lowers to a task::both statement rather than to let (_, _) = …, which would be a pattern that says nothing.let whose initialiser is not a bare call is now weighed, and its touch set is the union of every call in it: f("a") + g("b") performs two operations and is compared against the statement next to it on both. Values are taken apart where they are built out of literals and calls - arithmetic, a cast, a tuple - and refused everywhere else, each refusal naming what it was.x = 1 changes a name without binding one, so the data-dependency test - does the later statement mention what the earlier one bound - would find nothing to compare. A shape whose dependencies the analysis cannot see is a shape it may not read.fs::write(p, d) fails, the failure leaves the function and the sequential program never performs the statement after it - so starting that statement early performs work the program as written might never have performed. The narrow analysis reached this case and did not refuse it; a bare expression statement is what makes it ordinary.lets hid this, because a let cannot be a block’s value.??, 9 something with its own control flow, 6 the function’s own value, 2 a written type. The load-bearing row is the third: D4’s fail-closed polarity doubled, from 14 pairs to 28, because the shapes were hiding it. The zero is now a statement about the programs - microsecond work written in method calls - and §8.4 already says the overlap could not have paid for it either way.catch handler was invisible. f("a") catch { … } is one expression, but mentions was collected only from the call’s arguments - so let b = fs::read(…) catch { a } after let a = … saw no dependency on a and overlapped, and the closure would have read a name the same let had not finished binding. The handler’s names count now, including the words inside an f"…" whose holes are unparsed, and names_in is total - no _ arm - because a variant walked past silently is a dependency the analysis cannot see, the one direction it may never fail in.touches set computed from a variable holding a path is already sound (an unnameable resource conflicts with its whole kind), but sending the variable to another thread is a decision about what may cross one, and ADR-033 does not make it. The refusal stays, and it now names the value rather than the call.println/println still keeps its order (D6, through D4 rather than through a special case), and user_parallelism = no still never spawns a thread. The expression-statement overlap is compiled and run, and the program prints what the sequential one printed and leaves the same two files behind.
rustc subprocess actually costs)rustc_interface::run_compiler was written; the measurement is the deliverable, and it is in docs/subprocess-cost.md with its machine, its method and its variance..nika file becomes a mod at the crate root of one Rust crate (Part I 9.1), so a program of any size is still one rustc invocation: the fixed cost is paid once per program, not once per file, and its share falls as the program grows. The one thing that would invert that is named so it can be watched - if ADR-003 D2’s build graph ever splits a program into several Rust crates, each crate is an invocation and the fixed cost multiplies. That, and not a bigger program, is when D3 deserves re-measuring.--backend bridge is the default backend, and it panicked on every input, at the first symbol the lowering interned: Ident::from_str reads the interner out of rustc_span’s SESSION_GLOBALS and execute never installed one, so scoped-tls aborted before a line of Rust was printed. It compiled, which is why it survived - cargo check cannot tell a lowering that works from one that dies on its first interned symbol. Five lines (create_session_if_not_set_then) and crates/nikaia/tests/bridge_backend.rs now asserts the whole path end to end: .nika in, a binary out, and the binary prints what the program says. A second test holds the refusal: let_assignment.nika is turned down by name rather than by panic, because that is what the narrow waist (ADR-003 D3) is for.rustc_ast::Crate D1 builds is print-only: every node carries NodeId::from_u32(0) (which is CRATE_NODE_ID) and DUMMY_SP, where rustc’s own parser writes DUMMY_NODE_ID and rustc_expand’s assign_id! asserts exactly that before handing out a real id. Handing this crate to run_compiler trips that assertion on a debug compiler and, without it, leaves every node holding the crate root’s id. The spans say the same from the other end: ADR-012’s diagnostics are translated from rustc --error-format=json about a generated file, which an in-memory compile does not write. So the exit costs a node-id discipline, a SourceMap and a second diagnostics route - not nothing, and nowhere written down until now.rustup, not the process. rustc-executor calls Command::new("rustc"), and on PATH that is the shim: it starts a 40 MB binary, reads rust-toolchain.toml, and only then execs the compiler. Measured as wall-clock minus rustc’s own -Ztime-passes total in the same process: the real binary’s process cost is 7.0 ms (hello_world) to 9.8 ms (1brc); through the shim it is 27.6 and 32.0. End to end on the one corpus program Bridge-IR can carry, nikaia --backend bridge takes 69.0 ms, of which the subprocess is 63.5 (92 %) and the shim alone 15.5 (22 %). Left undone on purpose: which rustc the executor invokes is a toolchain-identity decision, not a performance one - the shim re-reading rust-toolchain.toml per invocation is what makes ADR-001 D1 true at run time - and no record decides it, so it is reported rather than chosen.bridge-ir is 85 lines with no control flow at all - no if, no while, no match, no binary operators - so of examples/ and tests/samples/ only hello_world.nika reaches the subprocess. The eleven examples lower through --backend rust, whose output opens with a grammar! { } proc-macro invocation that nothing in Bridge-IR can express. The timings were therefore taken on rustc compiling the emitted Rust of the whole corpus, which is the same work the subprocess wraps and does not depend on which frontend printed it.rustc on PATH is a symlink to rustup, so the first process floor credited 21 ms of rustup to rustc; -Ztime-passes rounds to milliseconds in its text format, so the parse read as either 0.000 or 1.000; and “wall minus internal” taken from two different run sets put five of fourteen programs at negative process cost until both numbers came from the same run (median absolute deviation 0.2-0.8 ms after).
dsl … { … } eod body’s :name holes are now a type, and a call site is checked against it. ADR-007 D5 has said since it was written that this is the entire safety argument for embedding SQL rather than concatenating strings - a compiler that cannot say “this call forgot :active” has not checked anything - and nothing implemented it. The statement is written once, the parameters arrive at the call site as named arguments after the ;, and forgetting one is a compile error against the .nika line.; is the whole protocol. db.prepare(query; id: 501, active: true): subject before it, configuration after it, which is Part I 5.1 applied to metaprogramming with no exception and no second calling convention. The statement may equally be the receiver (query.execute(; …), as Part II 10.5 writes it) - both spellings are the same rule and the check does not distinguish them.{id: i32, active: bool}; the body writes … WHERE id = :id …, which says nothing about i32. Where the type comes from is meta::parameter(name, type) (D4), and no grammar can declare one yet - so the emitter carries the names and takes the types from the arguments at the call site, monomorphised there. Writing i32 would have been the guess ADR-011 D2 forbids an emitter to make, and it would have been wrong for every statement whose first argument is a String.:id and :active share the type. Nothing observable turns on it - a struct literal names its fields, so a call site needs only the names it passes - and per-string identity would mean the emitter tracking which statement a receiver came from, state it has nowhere to keep....args: Self::dsl parses and lowers. The typed spread stands where the options stand, and it is exclusive with them: an option has a default and a DSL parameter does not, so a declaration that mixed them would have to say which rule applies to which name. It becomes a generic parameter in the emitted Rust, which is what “monomorphised per DSL string” is in the language below. Self::dsl in the result position names the same type, so a driver can hand the bound parameters back - an extension of D5’s spelling rather than something it decided, and the only thing a driver written in Nikaia today can do with the value.NK1112 and NK1113, each with the statement’s parameter list as its note - the note is the contract the call was working from - and a paste-ready way out, including the near-miss where there is one (did you mean \active`?`). A call that forgets one parameter and invents another says both things once, rather than one per build.;. It was parsed and dropped on the floor, which is why a dsl statement could not be given a parameter at all: Postfix::Method discarded the config zone. That was the load-bearing gap, and it was invisible - the call compiled and the arguments simply vanished.crates/nikaia/tests/dsl_parameters.rs lowers tests/fixtures/sql_statement.nika, compiles the emitted Rust with the rustc that built the test, runs it, and compares what it printed. The statement reaches the driver with its holes as written - SELECT name FROM people WHERE id = :id AND active = :active - because a deferred parameter is not string interpolation and the value may not be spliced into the source text (Part III, 15.3). The values arrive beside it.dsl html is untouched. Its :rows is an immediate capture from the enclosing scope (ADR-017, ADR-007 D4), and a test would fail if D5 had quietly turned it into a parameter. html is the one target this compiler is itself the grammar for; for every other, a body with holes is a deferred-parameter statement and a body without them is still refused - its meaning is the target grammar’s, and the refusal now says that instead of saying that deferred parameters are unbuilt.args.values() (Part II, 10.5) needs the parameters to have a type, which is meta::parameter again; SqlAst[Signature = {…}] is a spelling the parser has no rule for; and dsl <target> still does not resolve to a grammar. So the type that carries the names exists and a library that reads the values out of it by anything but their names does not. examples/fortunes.nika is unmoved: its postgres block has no holes.
nikaia build and nikaia run build a project, and the binary they produce runs. The CLI compiled one file. It now takes a directory, walks to the nearest nikaia.toml, translates it into a Cargo.toml under target/nikaia/build/, and drives cargo over it - so dependency resolution, fetching and linking are Cargo’s, which is the whole argument of D1: a language that cannot reach crates.io starts with no ecosystem, and those three are solved problems. A project with no runtime dependency goes from nothing to a running binary in 0.24 s; a project with regex from crates.io and a grammar in it takes 24 s the first time and 0.12 s after.nikaia --input x.nika --backend rust|interpreter behaves exactly as it did, and a test next to the new path asserts both backends still work without a subcommand - adding a subcommand to a CLI that had none is precisely the change that would have broken them.type = "rust" passes through and nothing else is invented. Part III 13.3 shows two dependency shapes. regex = { type = "rust", version = "1.5" } reaches Cargo with only the marker removed, because the marker is Nikaia’s word and Cargo would refuse it as an unknown key. http-server = "1.2" is refused with its reason: no ADR names a registry, a name space or a distribution format for a Nikaia package, and handing it to Cargo would make it fail on crates.io somewhere that explains nothing.[build.<target>] becomes a Cargo profile, and the panic strategy is not in it. opt-level and lto for the machine this build chose become [profile.dev]; panic is written there too and comes from target rather than from the author - unwind where the machine unwinds, abort where it traps (ADR-037 D1). A key [build.<target>] does not know, and a [build.<machine>] that names no machine, both fail the build and name themselves - the same rule [build] already had, for the same reason.RUSTC_WORKSPACE_WRAPPER, verified rather than asserted. Cargo applies it to workspace members only, which is why D1 names that variable and not RUSTC_WRAPPER: a crate from crates.io is compiled by the real rustc with nothing in between. The claim is invisible in the output - a dependency passed through a pass-through wrapper and one that never reached it produce the same rlib - so the wrapper writes down what Cargo handed it (NIKAIA_WRAPPER_TRACE), and the test over a project depending on regex asserts exactly one crate in the build was lowered, that it was the project’s own, and that nothing in the trace mentions regex, while Cargo’s own lockfile shows regex was resolved and linked..rs crate root, which is what makes the interception possible at all. The generated manifest names src/main.nika as the binary’s path; Cargo hands the wrapper a rustc command line with a .nika file in it; the wrapper lowers, substitutes the emitted Rust, and runs the real rustc. Cargo’s own rustc -vV probes go through the wrapper too and are passed on untouched.rustc emits names the generated Rust, which Cargo believes: an edited .nika source left the package fresh and the binary stale until every source that took part was put back into that file after rustc succeeds. Both now have a test - build twice, edit once.nikaia_std, winnow_grammar and winnow conditionally, so the generated manifest declares each only when the lowering actually writes it, and their versions are read out of the compiler’s own [workspace.dependencies] rather than written down a second time - a program that linked a different winnow from the one winnow-grammar was built against is a type error at every Stream bound.cargo, splitting a rustc command line and repairing a dependency file are in bridge-orchestrator::project and know nothing about .nika. crates/nikaia/src/manifest.rs grew to read [package], [dependencies] and the codegen tables, and Manifest::find now walks up through Layout::resolve - the cache’s root-finder - so a build and the cache entries it writes cannot disagree about which project a file belongs to.nikaia test, bench and fmt; and the D2 lockfile field for resolved dependency versions. That last is a real gap: Cargo’s Cargo.lock for the generated package records them and nikaia.lock does not, so the file that exists to answer “does this build the same thing for you as for me?” answers it today only about the Nikaia half. opt-level, lto and a dependency version deliberately stay out of the build-cache key - they change machine code and not the lowering, and the envelope the cache holds is the emitted Rust and the ledger, which Cargo’s own fingerprinting then covers (ADR-021 D1). A key that moved with them would be D7’s first failure direction.nikaia.contracts is now compared across two processes, for every example the bootstrap compiler can lower (10 of 11), at one thread and at four. D8 has said since it was written that the ledger is a pure function of (source tree, toolchain) byte for byte, and that the enforcement is “build twice and compare” - and nothing did.HashMap iteration into the ledger’s renderer: the test fails on it and goes green again when it is removed.readdir-order and locale rows rather than the hash-map one. It costs a second CI matrix leg and is not wired up.nikaia.toml carries the build switches, and a flag overrides them for one build. [build] target, [build] user-parallelism and [build] ordering were specified and unread - the choice lived only on the command line, which makes a project’s setting something every invocation has to remember. They are properties of a project, and a committed value is one a reviewer sees. --target, --user-parallelism and --ordering override for a single build, which is what a benchmark and a bug hunt need.[build] does not know fails the build and names itself. The manifest spells it user-parallelism and the flag --user-parallelism, so an underscore is the mistake a person actually makes - and a silently ignored key leaves the build at a default its author believed they had changed. cleanup-deadline is accepted without being honoured, because a manifest that follows the specification must not be refused by a compiler that has not caught up with it. [build.x86_64-linux] and [build.wasm32-unknown] are codegen tables rather than switches and are skipped, not rejected.user-parallelism = 4 in the manifest gets the same answer as --user-parallelism 4 on the CLI - how many threads serve a yes is the runtime’s to decide - rather than a type error about TOML that explains nothing.[package] and [dependencies], which by TOML’s rules makes them package metadata. They are under [build] now, where [build.<target>] already lived.--overlaps weighs 127 adjacent statement pairs across examples/ and refuses all 127; the emitted Rust is byte-identical under --ordering effects and --ordering strict for all ten files the bootstrap compiler can lower, and not one std::thread::scope is produced. But 101 of the 127 fall out before any touches set is consulted, on “one of them is not a let of a single call” - the analysis only looks at a shape almost nothing is written in. 12 are ADR-034’s conditional handler. Only 14 are the ledger’s fail-closed polarity doing its job. A zero produced that way measures the analysis, not the corpus.benches/overlap/ builds the two shapes the emitter produces for the same two reads and times them: 0.04× at 5 bytes per file, 0.13× at 64 KiB, crossover near 256 KiB, then 2.5× at 1 MiB and 6.75× at 8 MiB. Two spawns and two joins cost about 100 µs per pair, paid unconditionally. A fixed 100 µs tax on two operations that may each take a microsecond is the opposite of the goal these decisions serve.let x = f(…) so its silence means something, and find a vehicle that beats 100 µs - a cost estimate in the ledger, the pool a user_parallelism = yes build already has instead of raw spawns, or strict as the default with overlap as the opt-in. In that order; the second must not be decided before the first is done.user_parallelism = no, which promises the opposite. --user-parallelism no --ordering effects emitted std::thread::scope ten lines above the Parallelism::Off the same file hands the parser - and neither thread::scope nor rayon::join links on wasm32-unknown. The cause is a category error worth naming: whether two operations may overlap is a question about the program, whether a vehicle exists to overlap them with is a question about the build, and overlap_at consulted ordering and never the build. At no, effects now degrades to strict - the same degradation par_fold already makes to a sequential fold (ADR-009). --overlaps says which setting made its report hypothetical instead of reading as a promise. The predicate is user_parallelism and deliberately not “does this build have threads”: it has a second one at no too and always did, since Part I 8.4’s Runtime Sidecar offloads blocking I/O to a background thread on native (a Web Worker on WASM), safe precisely because user code never runs there - the exchange is ownership-transferring message passing.nikaia_std::task::both, which is rayon::join on the pool the program already has. Not mainly for speed: std::thread::scope asks the OS for two fresh threads per pair, so a handler overlapping two reads across a thousand concurrent requests asks for two thousand threads, and nothing in the lowering bounded that. The pool’s ceiling is the pool’s. It also means the emitter names one std function instead of spelling a scope, two spawns and two joins into every program - the next change of vehicle is a std change.thread::scope) to 46 µs (rayon::join), but the table over real reads does not move at all: with blocking I/O the second closure has to be stolen, which wakes a sleeping worker, which is the same futex wake spawn pays. The floor is thread wake-up latency, not thread creation - an OS number no user-space vehicle removes. So the overlap can only ever pay for operations in the millisecond range, and every program in this repository does microsecond work.user_parallelism = yes uses, not of the decision. ADR-033 may be worth more at no than at yes. A prediction and not a measurement - there is no event loop to time - but it redirects what to build next: not a faster thread vehicle (§8.4 ruled that out), the runtime no deserves.benches/overlap/run.sh is the number to beat, kept as plain rustc-buildable std so it can be read next to the generated Rust.allow_parallel, and the compiler says why instead)seq is refused, and the reasoning is written down because it will be proposed again. try_join { } is out twice over: it names what the compiler already does, so writing it would make the fast path the thing you ask for - the design ADR-033 exists to invert - and tokio::try_join! means cancel the others on the first error, the opposite of D6. Borrowing the name would borrow the wrong expectation.allow_parallel { } serves exactly one refusal, and that one has a clearer spelling already. Of everything the analysis turns down - a data dependency, the same resource, a resource it cannot name, a non-literal argument - only a catch that can return is a case where the programmer knows something the compiler does not and the program is right either way. And catch { return } conflates two things: what to do about the failure, and whether to carry on. Separating them (catch { Kunde::unbekannt() } then if k.ist_unbekannt() { return … }) is not merely equivalent but better, and it overlaps. A keyword whose only purpose is to rescue a phrasing that has a clearer form is a second way to say one thing - which is what ADR-022 removed a lambda form for.nikaia --input x.nika --overlaps. Every adjacent pair, which of them run together, and for the rest the reason - naming the file two statements collide on, the call nothing describes, or the handler that can leave the function together with what to write instead. A refusal nobody can ask about is the trap the keyword would have been an escape from; this is the asking. Asked for and never volunteered, because a paragraph per pair would be noise in exactly the programs that are fine. It explains a decision rather than changing one, which is what --trust already does for provenance.may_overlap returned a boolean; it now answers with a Verdict (and an Accounted for a statement it cannot reduce at all), which is why the report can say both reach file \log.txt`, and one writes it` rather than “not accounted for”.seq and ordering = "strict" - rather than permissions. A narrower and more honest surface than three block forms.f"…" interpolates, "…" is text)"{ margin: 0 }", "\d{3}" and every line of JSON a program printed were all format strings. examples/json/src/main.nika - a program whose entire job is to emit braces - was written print("{{}}") to print {}, and examples/1brc.nika ended on println("{{{body}}}"), three braces of which two were tax..nika files; 56 hold a hole, 154 are plain text. Three quarters of them were templates that interpolated nothing, and every string that wanted a brace paid for it."x" vs format!), the type branches (ADR-024 D5 made "a {b}" a String and "a b" a view), and since ADR-032 every analysis has to open every literal to find out. The only party who could not see the answer was the person writing the string.dsl html { … } is marked, a grammar is marked, a spawn is marked. The bare string literal was the one place the rule was forgotten - which is how ADR-017’s templates inherited ADR-032’s blindness without anyone noticing.f"…" has code in it; "…" is text (ADR-035 D1). {{/}} stay the escapes inside an f"…", where there is something to escape from. print("{}") prints {}."…" is a view, f"…" is a String whether or not anyone put a hole in it. That retires ADR-032 D5, under which adding a brace to a message silently changed its type.f and the quote are one token (D4), so f "x" is a variable beside a string and whitespace never decides what a program means. A template keeps its bare braces - the construct is the mark. And f"…" is unreachable where a constant is required (a Kap 5.1 default, a match pattern, a grammar literal): the grammar says so rather than a later pass discovering it.println("hallo {name}") still compiles and now prints {name}, and Part III C.1 calls a silent change of meaning a compiler bug - so NK1111, the first thing this checker warns about rather than refuses. It cannot be an error: "{ margin: 0 }" is correct CSS, and refusing it would break the property the checker rests on. So the test is narrow - the braces must hold something that parses and resolves to something that is here, a variable in scope or a function a ledger knows. It is removed one release from now.f"…" into str_lit as a second alternative silently dropped ` " ` from the parser’s “also possible here” list - a rule whose body is one sequence reports what it can start with, one built from alternatives did not. f_str_lit is its own rule, and the list now carries both ` " ` and ` f" `.lets whose calls reach different files now run at the same time. ADR-033 §6’s first increment, end to end: the ledger gained a touches column, contracts/order.rs decides whether two statements may overlap, and the emitter lowers a pair to std::thread::scope. std::thread::scope and nothing else - std’s I/O is blocking Rust behind fs::read, so overlapping it means threads, and a scoped join needs no runtime and no dependency.tests/ordering.rs compiles the emitted Rust and runs it, and the program prints what the sequential one printed. A lowering that produces plausible-looking Rust which does not build is worth nothing.println has no touches, which is also what keeps two printlns in order without a special rule for it); a handler that can return; an argument that is not a literal; and a block’s tail.called Result::unwrap on an Err would be the lowering putting its own words in the program’s mouth.--ordering strict turns it off (D8), and the switch is real rather than decorative: the same source lowers to exactly the program this compiler emitted before any of this. The nikaia.toml key of Part III 13.3 is still specified and unimplemented, so the choice lives on the CLI today.let a = fs::read_to_string(p) catch { return } is a second way, and it is the shape real Nikaia code is written in: both statements are unconditional, neither is inside an if, the touch sets are disjoint - and if the first read fails, the sequential program never performs the second one at all.break and an early return are three more, and the sentence covers all of them where a list would not have.ordering had to become a build-cache dimension. It changes the emitted Rust, so a key without it would let --ordering strict be served the overlapped artifact a previous run recorded - a cache handing back a program nobody asked for. ADR-021 D5’s own rule (“build-time choices reach the key”), met from a new direction.throws function body has a statement loop of its own, separate from a block’s, because its last statement may need wrapping in Ok(…). Two loops deciding independently what may be reordered is two rules waiting to disagree, so the pairing lives in one helper both call.fs::read_to_string(p) catch { "" } emits Rust that does not compile, because the handler is a &str and the call hands back a String (ADR-024 D5’s divergence). It fails identically with and without this change, and the tests here write catch { "".to_string() } to step around it rather than to hide it.let a = fs::read("x") beside let b = fs::read("y") costs 40 ms rather than 80, and nothing is written to get it. ADR-033, Part I 8.1.1.fs::write: it is I/O and may pause. Two writes to different files are indistinguishable, not because I/O is unknowable but because nobody wrote down what each call reaches. The ordering problem is a resolution problem of the effect system, not a safety property of parallelism.& and &mut are read and write; what is missing is only that a file, a socket, stdout and a Locked value are not yet things that can be pointed at. Not a new mechanism bolted on - the one the language already rests on, over a larger set.println/println stays ordered because both touch stdout - no special case), the no-pausing-while-locked rule of 12.2, and race freedom. The last is the interesting one: Nikaia’s freedom from races rests today on Lite being single-threaded, which is why it is a property of a profile rather than of the language. Under a touch model it is a property of the language, and the profile divergence loses its worst instance.seq { … } says an order the compiler cannot see - two calls to different addresses of one service. A line, visible, rare. And the whole thing is switchable: ordering = "strict" in nikaia.toml (D8) restores the written order everywhere, not as a temporary aid but as the escape for a project that does not want this.if x { lies(a) } else { lies(b) } starts one of them - burning a request that was never wanted is the opposite of what this is for. And a loop over ten thousand files must not open ten thousand descriptors; unbounded parallelism is not generosity toward a machine.touches column, no ordering key, no seq, and an emitter with no notion of a dependency graph. The first increment that would do anything end to end is named there - two unconditional fs::read calls with disjoint literal paths, lowered to a concurrent join.strict becomes the default rather than the escape."{…}" was invisible to the entire compiler. let n = add(1) is NK1101; the identical println("{add(1)}") lowered without a word. The same held for a dsl html template’s holes (ADR-017). The cause is one line of history: a hole’s expression is parsed in the emitter, on the way to Rust, so “parsed” and “analysed” had come apart for one construct and everything upstream was blind to it by default.sync is inferred from what a body calls (ADR-027), and the walk did not descend into holes. So pub fn greet() -> String { return "hello {io::read_to_string()}" } was recorded sync = "inferred" - a false claim, in a file a library ships, that a pausing function cannot pause. A consumer reads that and puts the call inside access or par_iter. ADR-027 D2 and ADR-010 D1 both name this direction the dangerous one: an analysis that fails open is a vulnerability generator, and this one did not merely fail open - it affirmatively claimed the safe answer for an unsafe body.emit::literal_expressions answers “what code is inside this expression?” for interpolated strings and for templates - including the holes inside a template’s for body and the collection it iterates - and every analysis calls it. Re-parsing holes at each call site is how the blindness happened the first time, and ADR-017 inherited it two ADRs later without anyone noticing, because there was no place where the question could be asked.== rather than merely both errors - equality is the property, not “an error appears”. The sync check holds an asserted sync to its holes too, so NK2202 names a pausing call inside a literal (D4).Ty::Unknown in ADR-024: the absence of a claim, not a claim of absence."a {b}" is a String and "a b" is a view (D5), which falls out of the checker looking at the literal at all.std.contracts or any example’s ledger moved. Nothing in the corpus was depending on the blindness, so the gap closes without any existing claim changing.Expr::LitStr, and with it the emitter’s measured string fast paths, the golden fixtures and the error corpus. It can be done later without any decision here changing meaning.sync from bodies and could not vouch for a method call; ADR-028 resolved method calls and could not type a lambda’s parameter; ADR-029 gave the ledger function types and still could not say what and_modify hands its lambda. In .and_modify fn { a.add(m.temp) }, a should be a &Stats - and Stats comes from the map, HashMap[&str, Stats], which the signature language had no way to refer to..entry is called on HashMap[&str, Stats] and .and_modify on a bare Entry. The information existed at the receiver and was dropped at the result. Nothing had to be inferred that was not already known - it had to be carried.$V (ADR-031 D1): HashMap::entry is (&HashMap[$K, $V], key: ?) -> Entry[$V], and at a call site the receiver binds the variables and they are substituted away. HashMap[&str, Stats] → Entry[Stats] → fn(&$V) is fn(&Stats) → a is a &Stats → a.add(v) resolves to Stats::add, which is pure. The sigil is not decoration: T, K and V are perfectly good type names, and a rule that guessed from capitalisation would silently turn somebody’s type into a hole.?, never a name (D2), which is the whole safety argument and why this does not contradict ADR-024 D4. D4 erases a Nikaia generic because a T that survives into a comparison makes the checker report that i32 is not T. A variable here cannot survive: it is bound and replaced, or it is Unknown. HashMap::new() says nothing about what it holds, binds nothing, and the chain stops helping rather than guessing.? stays for what flows in (D3), and this is a distinction rather than caution. What flows out is a promise the ledger makes, wrong on its own account. What flows in is a constraint on somebody’s program - and Rust’s HashMap<String, V>::get takes anything the key borrows as, so counts.get(fragment) in k-nucleotide.nika is correct and key: $K would have rejected it. That is the one thing the checker may not do, and there is now a test named after exactly that program.sync declaration from the examples and count what comes back: 9/18 after ADR-027, 14/18 after ADR-028 and ADR-029, 18/18 now. 1brc 5→7, access-log 4→6. Every sync written by hand in this repository is now something the compiler works out for itself - the keyword is an assertion you write where you want a property held, and no longer something a program has to carry to be usable.use parsed and lowered to a comment. use utils now brings in utils.nika beside the entry, every module becomes a mod at the crate root, and stock::total(…) in Nikaia is stock::total(…) in Rust - so ADR-011 D2 is untouched: nothing resolved it. What changed is that the compiler knows which files take part, which is a question about the file system rather than about names.std.contracts has had module-qualified keys since the day it existed (fs::map, io::lines, text::digit_value), and everything built on the ledger resolves a call by asking for a::b - the sync check, the type checker, provenance, Kap 5.1’s options, ADR-025’s fallible loops, ADR-027’s inferred sync, ADR-028’s method resolution. A program’s own ledger takes the same shape, so none of them learns a new trick. The file they ask simply has more in it.pub becomes pub, so Part I 9.2’s privacy is enforced by the language below rather than re-implemented - the same move ADR-017 D2 made with html::Render. And NK1110 says it in Nikaia’s words first, because Part III C.1 calls an untranslated backend error a bug in this compiler. It knows a call crosses a boundary without knowing where it is: only a call written module::item can be from another file.pub on every struct field, whatever the source said - invisible in a single file, and across a boundary it silently granted access 9.2 denies. And pub on a field did not parse at all, though 9.2 specifies it, so the one thing that would have made the first bug visible was missing too. Both fixed; the golden fixture that had frozen the old behaviour is regenerated."{utils::double(21)}" was read as the expression utils written with the format specifier :double(21). A hole in an interpolated string is Nikaia source, and Nikaia source names things across modules. :: is a path now.sync is a fixpoint over the call graph (ADR-027 D1), and a call graph that stopped at a file boundary would answer differently depending on which file was read first. Modules are recorded in name order, not in the order their imports were discovered.rustc message about the third module lands in the third module.examples/inventory/ is the first program here made of more than one file - the same program examples/report/src/main.nika is, in three. Both are kept on purpose: a reader can see what a boundary buys and what it costs on a program small enough to hold in the head. The example harness reads a program rather than a file, so an example may be one file or several and the tests do not care which.NK1110, because the ledger records a field’s type and not yet its visibility; and a hole in an interpolated string is not type-checked at all, because it is parsed in the emitter - pre-existing, and now worth naming.and_modify, or_insert_with and map signatures and deliberately no sync, because a ledger could not say “it depends”. That was honest and it rejected correct programs: fn ordne(xs) { xs.sort_by_key fn { a } … } sorts a list by a key and cannot pause by any reading of it, and calling it from a sync function was error[NK2202]: ordne can pause. Since access, access_all, par_iter and a scope’s tasks all gate on a sync lambda, the commitment read, where it bit, as no map, filter, fold or sort_by_key inside a lock, over any lambda at all.sync = "from(f)" (ADR-029 D3) names the parameter that decides - the same shape returns = "borrows(a | b)" already uses for “depends on a parameter”, which is why it needed no new grammar in the file. A caller reads it as “this call adds no pausing of its own”, and that is sound for one reason worth writing down: the lambda runs during the call, so its body is part of the function that writes it and that function has already counted its calls. from adds nothing because there is nothing left to add.@detached - breaks it, because then the lambda’s calls belong to nobody the caller is counting, and a pausing body would slip into a lock with the ledger saying it could not. The ledger cannot spell @detached, so the rule is enforced rather than assumed: a test walks every from entry std ships and requires it to name a parameter that is a lambda. The day std gains a detached one, that test is where it is noticed.fn(…) (D1) - fn(&Stats), fn(), fn(&str, i64) - saying what a lambda is handed. What it hands back is deliberately absent: nothing needs it, and a spelling is easier to add than to change. Only a ledger writes one: Nikaia’s grammar has no function-type syntax, so no source program can declare a parameter of that shape - which closes the hole where a function passes its own lambda parameter on, rather than leaving it open.fn { a.add(t) } records no parameters at all in the AST, because Part I 5.3 settles how many arguments a lambda takes from which of a, b, c its body mentions.sync from the examples, count what comes back - this changes nothing: 14 of 18, before and after; 20 of 43 functions corpus-wide, likewise unchanged. No program in this repository is unblocked by it. What it removes is a false rejection, and the smallest program that shows it is now a test: ordne above compiles and is sync = "inferred" where it used to be NK2202. The case is that the corpus is eleven mostly parser-driven programs and the rule this lifts applies to a category rather than to a program - and a reader who wants a measured crossover before believing a change is right to want one, and should know this one does not have it yet..and_modify fn { a.add(m.temp) }, and D2 types a from a signature that says fn(?). It cannot say fn(&Stats), because Stats is the type argument of the map and the signature language has no type variables. So the next question is one question: may a std.contracts signature refer to its receiver’s type arguments? It would be the first thing to add inference to the ledger’s type language rather than vocabulary, which is why it gets its own decision rather than being smuggled in here.
while exists)while was in the specification and not in the compiler, and it failed in the way that costs most: silently. Part I 3.3 documents it with a worked example; the parser had no rule for it, so while n < 5 { n += 1 } read as three statements - a variable called while, a comparison thrown away, and a bare block - and lowered “successfully” to while; n < 5; { n += 1; }. What a reader got was rustc complaining about a file nobody wrote, which Part III C.1 calls a bug in this compiler. It is one statement now, it lowers name for name (ADR-011 D2), and its condition is type-checked like an if’s (NK1108, with a note that says which loop it is about).while must be one statement and not three. That is the bug that was there, and a test that only checked the emitted Rust would not have caught it.while now appears in the list of what may begin a statement, because it may.loop is not added. It has been on the roadmap since that file was written and it is not in the specification - Part I 3.3 has while and for and nothing else. Whether the language wants an unconditional loop is a question to answer before implementing one, and the roadmap now says so rather than listing it as work.while does not (it does now). Both boxes are checked, with what is actually still open under them - the Bridge path, which carries let and an expression statement and refuses every other kind.sync from bodies and refused to vouch for a method call, because stats.add(5) names add and says nothing about what stats is. It recorded that as future work: “a method becomes resolvable the day its receiver has a signature written down”. The type checker had been resolving method calls on every build the whole time - receiver type, ledger lookup, contract - and nobody was reading the result. ADR-028 reads it.for iterates a fallible stream, because answering it means inferring a type and the checker is the module with types. This is the second instance: the alternative was giving sync.rs a second type checker, obliged to agree with the first.Ledger::infer now reads the declarations, runs the checker against that ledger, then infers sync using its answers. That is only valid because the checker reads signature, fields and iterates and never sync - so resolving a method against the half-finished ledger gives what resolving it against the finished one would. the_checker_does_not_depend_on_the_sync_it_helps_infer runs the checker twice, once against each, and compares all three of its outputs. Teach the checker to read sync one day and that test fails and says why, instead of the ledger quietly ceasing to be a pure function of its source.Expr::Index claimed nothing at all, so in n-body.nika let b = &self.bodies[i] made b unknown, so b.x was unknown, so (dx * dx + dy * dy + dz * dz) was unknown, so .sqrt() could not be resolved and energy could not be shown to be the pure arithmetic it visibly is. One Unknown at the bottom of an expression erases everything built on it. Vec[T] and List[T] yield T, HashMap[K, V] yields V, and anything else still claims nothing - indexing text is a slice in some languages and a byte in others, and Nikaia has not said.std.contracts describes the collections and text the language below provides (D5) - Vec, HashMap, str, String, f64. ADR-020 D5’s mechanism (a library whose implementation is in another language writes its contracts down and reviews them like code) used for a second purpose. Every entry was observed being asked for by a program in this repository, and the discipline matters more than usual here: a missing entry only fails to help, while a wrong one makes the checker reject a correct program - the one promise it cannot break. Types are ? wherever there is no one type; the arity is claimed and checked, and the corpus is what holds that risk down.sync, deliberately (D6). Entry::and_modify runs the lambda it is given, so whether it pauses depends on what is in it, and a ledger cannot say “it depends” yet. It gets a signature - the checker resolves through it - and no sync, so a caller still loses its inferred promise. That is the correct answer rather than a limitation: sync = true there would be a promise about somebody else’s code. sync = "from(f)" is the shape that would say it properly, and it stays ADR-027 §7’s open question.sync declaration from each example that has one, rebuild, count how many of those exact entries come back on their own. 9 of 18 → 14 of 18. n-body went 0/3 → 3/3 on D4 alone. And the four that remain are one shape rather than four problems: every one is a method called on a lambda parameter (.and_modify fn { a.add(…) } - what is a?). Typing that needs the ledger to say what a function-typed parameter receives, which is the same missing vocabulary D6 met from the other side.Cargo.lock moves to c7dd91a (winnow-grammar#14): a rule with several alternatives now skips its leading whitespace once, not once per alternative. On nikaia --backend rust over 2000 functions (300 KB), under callgrind: 1,025.5 M -> 822.2 M instructions, −19.8 %, with branches −19.8 %, mispredicts −9.5 % and both cache columns flat. Every column that should move moves, which is what removing work looks like rather than buying a mispredict with instructions.# "…" label already hoisted it, for the unrelated reason that the label’s offsets have to line up, and that is where the effect showed: adding a label to one Nikaia rule was worth 8.8 % on its own.docs/history/staging-candidates.md named dispatch over literal alternations as “the actual prize”. One callgrind profile of the compiler, taken before writing any code, said otherwise: parse_WS_inner was 32.5 % of the parse and the class scan inside it 24.1 %, against 1.8 % for the sixteen-way alternation and 1.0 % for the six-way one. Whitespace was 56 % and the literal dispatch under 3 %. The alternation was the problem - through the whitespace it re-ran, not through the compares it made.rt::expected on E::RECORDING - skipping, on the pass that discards messages, work only a message needs - costs +1.4 %. And respelling WS as multispace0 (COMMENT multispace0)* trades +0.7 % instructions for −7.7 % mispredicts, which the arithmetic favours by about 2.6× and which was refused anyway: it changes what a parse error says, and an uncertain performance trade bought with a certain regression in messages is not a trade.sync is earned, not entered)sync was opt-in, and that was backwards. Part II 12.1’s rule - a sync function may only call sync functions - is right, and the default under it was not. A function got sync in the ledger only if someone typed the word, so fn bonus(score: i32) -> i32 { score * 2 + 1 } was recorded as “not sync”: a claim about a body the compiler had already read and knew better about. ADR-027 infers it instead - a function is sync when every call it makes resolves and every callee is sync - and the keyword becomes what @borrowed is in Part I 6.6: an assertion you write where you want a property held, not a mode you have to enter.access, access_all, par_iter, a scope’s tasks under Advanced and the panic hook all gate on a sync lambda. sync is not a niche marker for hot loops; it is the gate on every safe concurrency primitive Chapter 12 has. While it was opt-in, what such a lambda could call was “whatever somebody remembered to annotate”, the annotation chain ran as deep as the call graph, and nothing from another library could be annotated at all. The safe path was the narrow one, which is the wrong way round for the thing the language is for.sync only where it can prove it - the same unresolvable method costs the function its claim. The entry is shipped (13.5), a consumer reads it and admits the body into access, and a wrong sync there is a pausing body inside somebody else’s lock. ADR-010 D1 settled this polarity once already: an analysis that fails open is a vulnerability generator. Both live in one file and share one resolution rule; they disagree only about what an unresolvable call means.sync = true where the source asserted it, sync = "inferred" where the body implies it. A caller must not tell them apart and does not - both mean “cannot pause”. A diff must: withdrawing an assertion is a decision someone made, losing an inference is a consequence of an edit further down the call graph, and two events that print the same line cannot be narrated apart. Absent still means not sync, so nothing about how the file is read changes.examples/1brc.nika carries seven sync declarations. Strip all seven and rebuild: four come back on their own - Stats::{new, add, merge, mean}, arithmetic on fields with no calls to resolve - and three do not, because Summary::{new, record, merge} reach HashMap::new() and .entry(…), a path and a method no ledger names. The boundary is exactly D2’s, and the remaining three are ADR-024’s open work rather than this decision’s limit: a method becomes resolvable the day its receiver has a signature written down, and neither analysis has to change for that to help.a_constructor_makes_the_same_promise_or_does_not was written when the check first reported 1brc.nika and access-log.nika - a sync method building a value through a constructor that never said sync either. That constructor builds a struct literal and calls nothing; it could never pause. The test had frozen a missing annotation as if it were a caught bug. It now asserts the accept, and keeps its other half: a constructor that really can pause still takes its caller’s promise down with it. The rule did not move; the ledger’s answer to “can this pause” got better.sync”, take the claim away - rather than a least one, which would deny both functions the promise on the grounds that each is waiting for the other. Pinned by a test, together with the reverse case and with one proving the answer does not depend on declaration order (13.5 makes the file a pure function of source and toolchain; --locked compares bytes).inference becomes stage0-signatures+sync-bodies (D5). A ledger whose sync was read off a declaration is not one whose sync was read off a body, and 13.5 built that header for exactly this event.--locked now names the entry a changed line belongs to (D6). sync is the commonest line in a ledger now, and the diff was printing bare sync = true three times over with no owner - which says something moved and leaves you to find out what. It says whose contract moved.crates/nikaia/tests/measure.rs, ignored by default, lowers a .nika file with the real emitter, compiles it with -O and counts instructions retired under callgrind. Two workloads in benches/, because a template has two shapes that answer differently - template.nika is one growing table, page.nika is a static page rendered many times. docs/history/staging-candidates.md §5 said the programs existed and the harness did not; it does now, and a workload lives in benches/ rather than examples/ because an example is a program someone reads to learn the language.html::escape is 65 % cheaper, and two of the three things tried made it worse. On 40 000 holes: the original (a five-element linear scan per character, twice) cost 466 M instructions; the table-driven version this repository’s own survey recommended - a 256-entry [&str; 256] - cost 541 M, 16 % worse; a one-word bitmask cost 181 M; and the bitmask without the run-copying that looked obviously better cost 161 M. All five characters are below 64, so “is this one of the five” fits in a u64 and touches no memory, where a table of fat pointers is 4 KB to walk. This is ADR-026 §3.1’s pre-registered failure - “table-free is not automatically faster” - met from the other side, on the first candidate that tried it.dsl html template reserves what the compiler knows, and what that is worth depends on the template’s shape rather than its size: −3.4 % for a static page rendered many times, and −0.0 % for one growing table, the same constant 585 instructions at 200 rows and at 20 000. The reservation is a floor - every hole and every turn of a <for> adds to the result and none subtracts - so it needs no threshold and can never be too large. For a loop-dominated template that floor is a few dozen bytes of a megabyte, which is why the zero is structural rather than disappointing.docs/history/staging-candidates.md §6 ends on “a staging decision enters the compiler only together with a measured crossover”. Applied to the first two candidates it named, the rule overturned one recommendation outright and split the other into two answers. Both are now recorded there with their numbers, and the remaining candidate - dispatch over literal alternations, which lives in winnow-grammar-macros - has a harness to be measured with.A function may take named options with defaults, which Part I 5.1 has specified since 0.0.5 and nothing parsed. A ; divides a signature: positional data before it, named options after it, and an option always has a default - which is what makes it an option rather than an argument that has to be passed at every call.
fn request(url: &str; timeout: i32 = 30, method: &str = "GET") { … }
request("https://api.com")
request("https://api.com"; timeout: 60)
request("https://api.com"; method: "POST", timeout: 5)
method written first above is still passed second - which is exactly what the ledger records. signature gained the options with their defaults, so fs::write(path, data; append: true) is checked and expanded from the file std ships.fs::write has the two options its specification names, append and create, and they do what Part III 17.1 says: append keeps what is there, create: false refuses to make a file that is not there, which is how a program says it means to overwrite something in particular.NK1109: a call names an option the callee does not have, with the one that was probably meant. Also reported where the callee has no ; at all, which is a different mistake and says so.&str; it lowers to format!, and a format! is a String. Two spellings in Nikaia, two types below - and the checker follows the lowering rather than the syntax. Pinned by a test.examples/tally/src/main.nika uses both halves: it declares an option and passes one to fs::write, appending its summary to a run log.fs::lines and io::lines produced ADR-025: one half is about failure, the other about ownership, and only the first was a question about the language.for line in io::lines() performs a call per turn that nobody wrote and no signature declares - so there was nowhere for throws to attach, and pressing three answers (a line, the end, I could not find out) into an iterator’s two makes a failed read indistinguishable from the end of the stream. A ten-million-line file reports two lines, successfully. Part I 6.4 already refuses exactly that at the closing brace of a block, where a resource’s cleanup can fail, and calls it “a decades-old bug class in other languages”. So the rule was written and had simply never been stated generally: D1 - an implicit call that can fail fails the enclosing function, and the compiler makes you declare it. Cleanup is its first case, iteration its second, and a third (an operator, a conversion) is covered in advance.for try await and Zig writes while (try …); Nikaia takes their answer and leaves their marker.NK2701, and a range of its own (D5) - NK27xx, implicit calls, in the shape NK2601 set: this function can fail because a turn of this loop can fail · help: declare the error: add throws to this function.fs::lines and fs::bytes are removed from Part III 17.1 (D3), not deferred - the way ADR-009 closed G4. lines(path) was to open the file and yield tethered &str, so the value would own the buffer and hand out views into itself: the one thing an iterator may not do, and the reason the language below allocates a string per line for the same function. The shape that works is two calls - fs::map(path) owns the pages, .lines() borrows views of them - and that separation is the model Part I 6.6 and ADR-008 rest on. For a record-per-line file the language already has something better than a sequence of lines: @frame(boundary: "\n"), which is what 1brc.nika, access-log.nika and config.nika all use. None of them iterates lines.io::lines() exists, because a pipe is genuinely different: its bytes do not exist until they are read (ADR-019 D1), so the failure cannot be moved to the call. Its lines are owned, which 17.1 already specified for its own reason and which is also what keeps it expressible - an iterator may hand out views into a buffer it does not own, and never into one it does.examples/tally/src/main.nika, the tenth running example and the first whose memory does not grow with its input: one line alive at a time, however long the pipe. k-nucleotide.nika reads all of standard input, which is right for FASTA and wrong for a log that never ends; this is the other half.iterates = "throws" on a type contract, carried by std.contracts for io::Lines. Second extension, second time it was one key.for iterates a fallible stream, so for line in io::lines() and let s = io::lines() followed by for line in s are the same thing - matching on the name would have caught the first and quietly missed the second. The checker was built to report; it turns out to be what tells the emitter what to emit.std can write a file)fs::write(path, data) and fs::read(path) (Part III, 17.1). Every example in this repository ended at println, and a program that cannot produce a file is not a tool. write creates the file or truncates it, which is what write means everywhere; read is the bytes half of read_to_string, for input that is not text and where nothing downstream is going to cut a &str out of it.examples/report/src/main.nika, the ninth running example and the first whose result is a file: a stock list in, an HTML page out. It is also escaping.nika’s claim tested against data that was not written to make the point - the &, the " and the < come out of the input file, which is where they come from in every program that has ever had an escaping bug.crates/nikaia/tests/examples.rs gained wrote: beside input: and stdin:, and {output} beside {input}, so an example whose deliverable is a file is checked by reading it back - under both profiles, which must agree on it as they must agree on what is printed.std.contracts, and the bug it fixes. A parameter whose Rust type is a bound rather than a type is recorded ?. fs::write(path: impl AsRef<Path>, data: impl AsRef<[u8]>) accepts a String and a &str and a buffer, so writing &str there would make the type checker refuse fs::write(path, text) for a String - a correct program, rejected, which is the one thing ADR-024 promises cannot happen. fs::map and fs::read_to_string carried exactly that mistake and now do not. The arity is still checked; the type is not claimed, because there is no one type. Pinned by a test.std::fs)fs::lines and io::lines as -> Lines throws, as if the failure happened when the stream was opened. It does not: a read fails mid-iteration, and Nikaia had no form that said so. Neither function shipped here. Closed by ADR-025, above - and the first write-up of this gap got two things wrong that the ADR corrects: fs::lines and io::lines are not one problem but two (one about ownership, one about failure), and 1brc.nika is not built on .lines() at all - it declares @frame(boundary: "\n") and folds over the mapped pages, which is the language’s real answer to line-oriented input.fn request(url: String; timeout: i32 = 30, method: String = "GET") and nothing parses it. Found by fs::write, whose specified surface has append and create in exactly that position: they are not in std because there is no way to write a call that passes one, and inventing a spelling for one function is a spelling to keep or to break later.NK1xxx family - Syntax & types - had been empty since the catalogue was written: NK1101 a call passing the wrong number of arguments, NK1102 an argument that is not what the parameter takes, NK1103 a let that says one type and is given another, NK1104 a return (or a body’s last expression) that is not what was declared, NK1105 an assignment, NK1106 a struct literal’s field, NK1107 a field that is not there, NK1108 a condition that is not a bool.?, the absence of a claim. Half of std is still Rust (ADR-014) and a Nikaia program calls push_str, entry, chars and nth freely. A checker that had to answer for those would need a second frontend that reads Rust, or a guess - and a type checker that guesses reports errors that are not there, which is worse than one that says less. So every rule has the same shape: infer both sides, and report only where both are known and they disagree. The promise that buys is the one worth having: it never rejects a program that is correct, and what it catches grows as the ledger grows, without the checker changing.signature on a function and fields on a type join sync, throws and returns in the file Part III 13.5 already specified - inferred from the source for Nikaia modules, written by hand and reviewed for the Rust half of std, which is the same two-origin shape ADR-020 gave it. One mechanism, three sources, in the order a call resolves: this unit, std, and a package’s own ledger on the day one arrives. A generic parameter is recorded as ?, because T is a name that stands for a type rather than being one.examples/fortunes.nika put a &str literal in a String field. Nothing had caught it because that example does not run yet - it waits on the postgres DSL - so no rustc had ever seen it. That is the case the checker exists for: a program the backend has not compiled, checked in the language it was written in.&str, and Part I is corrected to say so (D5). 2.4 said in passing that let name = "Nikaia" makes a String, and 2.3 wrote let strictly_string: String = "Hello"; 6.6 says an allocation happens only where you wrote that you wanted one, which is the language’s central claim about large inputs and is incompatible with a literal that allocates at every mention. The literal is a view; .to_string() is how you ask for the other thing. This is ADR-013 D7’s rule - a conversion is written, never quiet - applied to the most common conversion there is.3 to i32 would make add(3) an error wherever add takes anything else, which is more lines than it would be right about.&str against String suggests .to_string(), two numbers suggest as, and a field that is not there is matched against the ones that are - with a distance that counts a swapped pair as one edit, because nmae for name is the typo this is for and plain Levenshtein charges two for it.? - that belongs to the parser backend and Stage 0 does not read it - and the rule’s declared type is enough for both checks that matter there.Type::method in this unit’s ledger or a library’s (fs::Mapped::deref matched by suffix, name-for-name as everywhere else), a for over a Vec[Row] binds a Row, and a constructor hands back the type it is on..nika file in the repository - eight runnable examples, fortunes.nika, and std’s own modules - and the build fails if any of them produces a finding. Beside it are the deliberately-wrong programs, one per code, which are what makes a clean corpus evidence rather than a tautology.fn: is gone)fn: expression is removed (ADR-022). It was four characters shorter than fn { … } and cost three things. It did not grow: a body that gained a second line had to change form rather than gain a line - not hypothetical, since ADR-013 D7 lists three lines of 1brc.nika rewritten from it, and the file still carries the comment saying so. It was a second way to write the same thing, differing from the block only in a delimiter, so ADR-013 D5 had already spent a paragraph on which to reach for. And its body swallowed the chain..route fn: handler(db) followed by .listen(":8080") lowered to route(|| { handler(db).listen(":8080") }) - the .listen landed inside the lambda, with no error and a different program. Both readings are valid expressions and nothing in the grammar can tell them apart, because there is no token between the body and the rest of the chain - which is exactly what a block’s } is. The specification’s own examples were written in the form that misparses: Part II 12.6 chained .map fn: … into .filter fn: … into .collect(), and Part III 17.1 chained .route("/") fn: … into .listen(":8080")..route("/x") fn { handler(db) }. That form had no grammar at all - there was a rule for a method with arguments and one for a method with a trailing lambda, and none for both - and it is unambiguous because the short form is gone.fn: and fn { … } differed only in a delimiter, while inside and outside the parentheses differ in where the argument goes, and the outside form is what lets a chain read down the page.fn: is an error that says what happened, because the form was in the specification and somebody will have it in their fingers: the fn: … form was removed (ADR-022): write fn { … }. Its body ran to the end of the expression, so a .method() after it landed inside the lambda - silently.examples/fortunes.nika parses in full. Its main was the last thing in the repository that needed the added form. What that file waits on now is the postgres block - a deferred-parameter DSL (ADR-007 D4) - and G6’s runtime binding; the syntax is no longer in the way.fn: Shorthand) and now records the removal; 5.3 gains the trailing rule. Part II’s par_iter chain, Part III’s server example, ADRs 009, 013, 017 and 018, four example sites and std’s own doc comment are written in the one form.
dsl html { … } eod is compiled where it is written. The body is known when the program is compiled, so it is split into literal markup and holes there, and what comes out is the string building a hand-written renderer would do. That is what makes the escaping a compile-time property rather than a call somebody has to remember. This is the half of G7 that was decided and not built.html::Render, and the compiler has no branch that emits anything else.<script> body, a <style> block, an HTML comment, an unquoted attribute or a URL attribute is refused with the position named and the reason given - a URL needs percent-encoding and a scheme check, not HTML escaping: javascript: survives every one of the five characters. Every illegal hole is named at once, because a template with three of them should say so three times rather than once per build. That check is what lets D1 be unconditional.html::Raw is the promise and Raw::new its one constructor; html::Render is that decision put where rustc can act on it: a Raw renders itself, text renders escaped, a number writes itself, and a type with no impl cannot be placed in a template at all. The Nikaia compiler emits the same call for every hole and does not need to know which case it is - which is just as well, since it has no type checker.{{ is a literal brace, the same rule an interpolated string follows, and the framing whitespace of the block - the newline after { and the indentation before } eod - is not markup and is removed.examples/escaping/src/main.nika: an eighth running example, and the smallest program that shows all three decisions. Four rows: three whose text is escaped (< and & in a name, quotes around a note, an apostrophe, a </td> that is not a tag) and one the program built as markup and said so with a type.<for row in :rows> … </for>, written as an element because the file is markup and an editor that highlights it keeps working - a second syntax in a file that already has one is a second thing to know. :rows carries the colon because it is captured from the enclosing scope (ADR-007 D4): that is where the template’s names end and the program’s begin. It becomes the loop of the language below, over the captured collection, so it borrows rather than copies; the position check runs through the body, because a hole in a <script> does not become safe by being repeated. fortunes.nika’s render lowers and runs today - that file now waits only on G6 and on fn: in a method chain (ADR-013 D5).Shared[postgres::Connection] did not parse, and fortunes.nika found it the moment its template stopped being the first thing that failed. The whole path is interned as one name, because that is what the name is to a compiler that lowers name for name (ADR-011 D2) - nothing here resolves a module, and a path can therefore never collide with a struct the file declares."{f(\"a\")}" handed the parser f(\"a\"), which is not an expression. A hole is Nikaia source written inside a string literal, so the escaping it carries is that literal’s: the two characters the enclosing string had to escape are undone before the hole is parsed, and every other escape keeps its meaning.dsl block also parses now: Expr::Dsl had been in the AST since an early draft with nothing producing it. A target other than html is refused with what it would need instead - a dsl sql { … } is a deferred-parameter DSL (ADR-007 D4), and saying so beats lowering it to something that reads like a template and is not one.
std’s ledger says which of its calls are sources and who chose their bytes (ADR-020: a file the operator named, a pipe they connected, the arguments they typed), contracts::trust joins them over the program, and the emitter names the map accordingly. Nothing else about the map changes - same table, same API, same full-content equality (ADR-010 D5).docs/handoff.md and ADR-011 §4 called this the largest single item left and predicted 22 %; it is more, because the prediction was taken against a slower baseline.std, which states all of its sources, so today there is no unresolvable one.nikaia --trust prints where the bytes came from, which source said so, and which hash the maps got - the choice is visible, never a mystery (D7).std’s ledger says about the sources that source calls, and the compiler’s fingerprint (ADR-021 D3) hashes std.contracts by name alongside the compiler’s own sources. So the key moves when the answer can move, without provenance being a key field of its own.std::hash: FxHasher and the TrustedMap / TrustedSet it backs. It is the hash rustc uses on its own tables, and it is in std rather than behind a dependency for two reasons: it is twenty lines that sit on the hot path of every Nikaia program that aggregates anything, and a program whose hash function comes from a crate it did not choose is a program whose iteration order and worst case can change under it. One thing the tests pin because it is real and easily mistaken for a bug: the empty key and the single zero byte hash the same, which costs a bucket comparison and nothing else - a map compares keys in full - where a length mixed into every hash would cost a multiply on every lookup to fix it.std has no untrusted source yet: http, net and db arrive with the modules that have them. The lattice is tested against a ledger that does have one, so the join is checked rather than assumed on the day one appears.docs/handoff.md §4’s second open item closes without a change. The question was whether the measurement that rejected keying 1BRC’s station table by Symbol - 617 instructions per row against 514 for a plain fast-hashed map - had been an artefact of an unsized interning cache, since ParseContext::expect_distinct_keys(n) exists upstream and Nikaia does not call it. It was not. Callgrind over intern(until(";")) on 100 000 rows, sized against unsized: at 413 distinct keys, 1BRC’s station count, sizing saves one instruction per row; at 5 000 it saves 60. The default 512 slots already holds 413 keys without thrashing..nika file would need a way to say a key count - an attribute on the grammar, the count being the author’s knowledge and not the emitter’s to invent (ADR-011 D2) - and that is worth designing when a program wants thousands of distinct keys, not before. Upstream winnow-grammar#13 puts the numbers where the method is documented, so the next person reads them before calling it.sync is checked)sync function may only call sync functions, reported as NK2202 on the .nika line, with the source line and a caret under it - the shape --explain has shown rustc’s diagnostics in since ADR-012, because a rule the compiler checks itself should not look different from one it relays. The keyword has parsed since ADR-013 and the emitter wrote // … Not checked yet. above every one of them; it does not say that any more.io::read_to_string from a sync function is only a violation if something says that function can pause, and std.contracts is where it says so - which is ADR-019 D2 becoming a check instead of a paragraph.io::read_to_string into a library’s ledger. A method call it cannot, because that needs the receiver’s type. So the check is conservative in the permissive direction - it never rejects a program the rule allows, and does not yet catch every program the rule forbids. Worth having and worth saying: an unchecked promise catches nothing at all, and what it does resolve is what the rule exists for, since fs:: and io:: are named rather than called on a receiver..and_modify fn { … } runs during the call it is given to, so what it calls, the function around it calls. spawn is the one that is not walked - its body runs later and elsewhere, which is what a detached context is (Part I, 5.4).1brc.nika and access-log.nika each had a sync method building a value through a constructor that never said it was sync either. Both were pure computation all along and both now say so - which is the check doing exactly what it is for, on the two examples that have been in the repository longest.nikaia --backend rust now writes nikaia.contracts beside its output, and --locked verifies it instead of writing it - the CI line the specification names, exact because the file is a pure function of source and toolchain.sync (Part II 12.1), throws (Kap 7.1), what a result may point into (returns = "borrows(a | b)"), and for a type whether it is @borrowed and which of its fields tie it to the input - including through another type that does, which is the transitive half of Part II 10.6. Recording only borrows would have meant a second file for the second question, with the same determinism rules and the same distribution story.sync means not sync while an absent entry means nothing is known and a caller may not assume. That distinction is the whole reason the file is worth shipping rather than deriving.inference = "stage0-signatures" so the two are never mistaken for each other and --locked fails on the difference rather than accepting the weaker answer as the stronger one. The toolchain recorded is Nikaia’s version, not rustc’s: these contracts are decided here and never by the compiler this one emits code for.std ships its contracts)crates/nikaia-std/std.contracts, the ledger a program’s compiler reads when the program calls io::… or fs::…. Part III 13.5 has always said a published package ships its ledger; this is std doing it.crates/nikaia/tests/contracts.rs regenerates them and fails if the file has drifted. The modules still written in Rust cannot be inferred by a compiler that does not read Rust, so their entries are written by hand and reviewed like code - the honest shape of a library whose implementation is half in another language, and a share that shrinks as ADR-014 proceeds. A second test fails if a module reaches the prelude with no contract at all.text.nika gained a sync. Writing the ledger showed that std’s one Nikaia function did not declare what it plainly is: pure computation that cannot pause. That is the file doing its job on its first day.sync function may only call sync functions - is unenforced, and ADR-019 D2 leans on it in specification while the compiler says nothing: a call to io::read_to_string from a sync function is exactly what it forbids, and the compiler could not know what io::read_to_string is. Now it can look it up. ADR-010’s provenance sources are contracts of the same kind, and a type checker’s signature store is this file with types added rather than a new one.std::io. Part III 17.1 specified std::fs down to seek and Cleanup and said nothing about the input a program in a pipeline has - so Nikaia could open a file by name and could not read what was piped into it. The Computer Language Benchmarks Game feeds every one of its text programs that way.map, no seek, no length, and no second read of the same bytes. fs::map hands back pages that existed before the program asked for them; standard input’s bytes do not exist until they are read, so a program that wants views into its input owns the buffer first. The omissions are the specification - a seek that worked while the input fit in a buffer and failed when it did not would be worse than none.sync. No async on the signature, no await at the call, the same as std::fs; under Lite the event loop runs another task while the pipe is empty, under Advanced the read may resume on a different thread, and neither is visible in the source. What is visible is the rule that matters: a sync function cannot call it (Part II, 12.1) - which is not a restriction invented here but the existing rule doing the job it exists for, since a par_iter body is sync precisely so that no core ends up waiting on a pipe.lines() yields owned text where fs::lines yields views, because a file’s line can be a view and a stream’s cannot; there is one standard input, so the module is functions rather than a handle two tasks could hold; and provenance follows the file rule - Trusted, with trusted: false at the entry for the request-body-on-stdin case, so ADR-010 keeps one rule about where bytes came from rather than two.read_to_string and read are implemented; lines and bytes are specified and are not, exactly as fs::open and fs::seek are.examples/k-nucleotide/src/main.nika: the CLBG benchmark, reading FASTA from standard input and reporting how often each short fragment occurs. crates/nikaia/tests/examples.rs grew the ability to feed an example on a pipe rather than a path, because a pipe is what the example is about.1brc.nika maps its file and copies nothing at all; here there are no pages to point at, so the program owns its buffer - and everything after that first copy is the same, which is the part worth seeing. The fragments counted are thousands of views into that buffer (&seq[i..i + k]), not strings, so a sequence counted at k = 12 builds a table of pointers and lengths.> and the lines under it, and not(">") is what ends a section’s body without the repetition needing a terminator. The two stable sorts of Part I 4.5 state the benchmark’s compound order - by fragment ascending, then by count descending.return inside an if stopped returning)if in statement position had its branches emitted as tails. That is right for let x = if c { a } else { b }, where the branches are the value, and wrong for a guard: the last statement of a branch was written as the branch’s value, so if n < k { return counts } came out as if n < k { counts } and the function carried on. Found by examples/k-nucleotide/src/main.nika, where the types happened to disagree so rustc caught it - in a function returning nothing it would have compiled and quietly done the wrong thing. Pinned in crates/nikaia/tests/expressions.rs, in both the plain and the throws shape.winnow-grammar 9180b3d -> d431ed1 (upstream #12). A class scan is a run of nothing more often than it is a run of anything: the implicit whitespace skip runs between every pair of elements of every syntactic rule, and in a language written without gratuitous blanks most of those find nothing. The scan paid its whole eight-bytes-at-a-time setup to report that the first byte was not in the class; it tests that byte first now.examples/1brc.nika over 200 000 rows goes 123.6 M to 120.0 M - against 119.4 M for a build with no word scan at all, so the guard recovers seven eighths of what the scan costs there while keeping everything it buys on long runs.docs/handoff.md §4’s first open item, and not the way that item proposed. The estimate was a break-even of six to eight characters and a threshold in the code generator; measured, the crossover is at three, and a class that always matches exactly one character is written as digit, which never reaches that code. The cost was in the row of the table nobody had measured.examples/n-body/src/main.nika: the Computer Language Benchmarks Game’s five-body integration, and the first example here that parses nothing. Five programs in a row that all begin with a DSL would say Nikaia is a parser generator; this one is arithmetic in a loop - sync methods, &mut self, indices and floats - which is the other half of what a systems language is judged on. It is also the only example whose numbers are not ours to choose: the CLBG publishes the same program in some thirty languages with an exact expected output, and ours matches it to the digit (-0.169075164 / -0.169087605 at n = 1000), which crates/nikaia/tests/examples.rs checks under both profiles.for i in 0..5 since the first draft and the bootstrap compiler could not parse it, so an index loop had no way to be written at all. a..b excludes its end, a..=b includes it, and a range binds looser than every operator in it - 0..n - 1 ends at n - 1, which is the reading a loop head wants and the only one that is ever useful.1.5e-4, 2e3, 9.54791938424326609e-04. That is how a program about physical quantities is written, and the same number spelled out in zeroes is how a digit gets lost in a diff. The rule is lexical, so 1 . 5 is still not a number.(a as f64).sqrt() came out as a as f64.sqrt(), a cast to a type nobody named, and (a + b).to_string() as a + b.to_string(). Both often still compiled, which is what made this worth a test of its own. The receiver of a ., the base of a [, and the operand of a ? are now parenthesised where they bind looser.1.5 parsed as the field 5 of a variable called 1. The backend’s ident accepts one by design and a grammar that wants otherwise says so, which the compiler’s own grammar had never done. It printed back identically - which is exactly why it survived, and why the corpus never caught it: the emitted text read the same right up until there was more after it, and 1.5e-4 came out as 1.5e - 4. not(digit) in front of the name costs a character comparison and closes it.examples/json/src/main.nika: a JSON document, parsed into a tree and printed back. The first input whose shape the grammar does not fix - config.nika’s tree is two levels deep because its rules say so, and a JSON value contains values to whatever depth the document goes. Three things follow that no other example shows. An enum is the tree: six variants, and every walk over it is a match with six arms that the compiler checks for completeness - the check a kind: &str field cannot have. Two cycles, neither declared: value reaches value through [ and {, and Json holds a Vec[Json] and a Vec[Member] that holds a Json; the view analysis of Part II 10.6 walks the type cycle to a fixed point rather than into it. And where zero-copy stops: a JSON string is not a slice of the input ("a\nb" is five characters in the file and three in the value), so Text holds the raw body - still a view - and unescape runs only on the strings a program asks about. That pays twice: printing the document back needs the raw text, so the common case never decodes at all. It is deferred decoding, the argument dec[T] already makes for numbers.char, which the language could not say)'n' was not something the language had, and char was not in the table of primitive types either - the type was reachable only because a type is a name. Decoding an escape is exactly the shape a match over characters has, so both halves are needed and both are here. The body is kept as written: the language below spells '\n' the same way, so the lowering is a transcription (ADR-011 D2) and nothing decides twice what it means.print and eprint, which are what println and eprintln always were minus the newline (Part III, 17.1). Output composed piece by piece had no way to be written: a pretty-printer that indents a tree cannot end a line after every fragment. json.nika is the first program here that needs it.: inside one separates, that {{ is a literal brace, and that an escape is not a hole.\u{…} escape)"\u{0041}" was read as a hole named 0041 and emitted a format! with an argument nobody wrote. A string’s body reaches the emitter as it was typed - the parser keeps the escapes rather than decoding them - and the interpolation scanner did not know an escape when it saw one. It now copies an escape whole, and the { of a \u{…} with it. Found by writing examples/json/src/main.nika, which is what the examples are for.winnow-grammar e222ae8 -> 9180b3d (upstream #11), and tests/errors/EXPECTED.txt regenerated. This is docs/error-corpus.md’s closing finding, and the only one that applied to all twenty-six rows at once: every message was a headline plus in <rule> lines, so a reader of at line 3, column 5 never saw line 3 - while a rustc diagnostic routed through nikaia --explain had carried a snippet and a caret since ADR-012. A position a reader still has to go and look up is half a diagnostic.expected `}`; found unexpected token `temp` at line 3, column 5
3 | temp: i32
^^^^
note: also possible here: `,`, `//`
in struct_item
render: the compiler’s own messages for .nika source, which is every corpus row, and the errors a generated program prints about its input. examples/access-log/src/main.nika’s catch now shows the rejected log line and points at the character the parse stopped at, and crates/nikaia/tests/examples.rs checks that end to end - a Nikaia program compiled, run on a broken file, and its message read.= help: lines.docs/history/staging-candidates.md follows ADR-022 §3’s claim that Tier 1 waits on nothing, and checks it against today’s code. The first finding corrects this project’s own suggestion: route hashing has no target - there is no HTTP server, route appears only in examples/fortunes.nika at specification level, and ADR-018 parked it. Also not candidates, each for a reason worth recording: match over strings (rustc already length-buckets and memcmps it), and character classes and terminator sets (solved upstream and measured - AsciiClass runs eight bytes at a time, until(a | b) already picks memchr/2/3 below four needles, and every Nikaia grammar uses at most two).emit/mod.rs:735, :732) and hands it on unchanged, because every parser decision is made in winnow-grammar-macros, an external git dependency. examples/json/src/main.nika:69 pays 8 failed compares for a Json::Object; the compiler’s own grammar has 35 literal-led alternatives. The obstacle is location, not difficulty - and Cargo fingerprints a git dependency on its commit, so testing needs a [patch].json.nika and the compiler’s own grammar do.std. The rule the survey lands on: a staging decision enters the compiler only together with a measured crossover, because otherwise the complexity is certain and the gain is not - and it is paid twice, in the language surface and in the generated code.Open - a value no other ADR here uses. Every one of the twenty-one before it is Accepted or Superseded; this one is a design space with two decisions in it and six questions, written as questions because they are not answered. An open ADR that quietly decides something is worse than none, since the next reader takes a sketch for a commitment.dsl syntax serves compile time and runtime and the context decides - but const does not exist at item level, in the parser or in ast::Item, so the discriminator that decision rests on has no representation. Meanwhile ADR-021 D2 puts every compile-time-read file’s hash in nikaia.lock and D7 makes it a key dimension: that dimension is implemented, tested, and has no producer. And the fact that shapes every option: GrammarAlt.action is an arbitrary Nikaia block, so running a grammar at build time is running user code at build time... is refused rather than resolved. Relative to the source file, inside the nikaia.toml root, and a path containing .. is malformed rather than normalised-then-checked - because a check that normalises first is a check that argues with a normaliser, which is where confinement bugs live. Left open inside that decision: a symlink pointing out of the root satisfies the rule as written and defeats its purpose, and resolving links reintroduces the argument the rule avoids.const is a different mechanism from general const, which lets the closed case ship without waiting for the open one.wasm32 target. The precedent is in this repository and it is measured: ADR-010 picks a map’s hasher from the provenance of its keys, nothing in the .nika mentions hashing, and it took 22 % of 1BRC’s instructions. Tier 1 waits on nothing in §4.std provides (ADR-010’s pattern; SIMD lives as hand-written Rust in nikaia-std, and the emitter carries no target_feature, no std::arch, no intrinsics), or the emitter generates the specialised code outright (the grammar pattern - a DFA as code rather than a runtime table is what it already does). With the caveats attached rather than left for a benchmark: a table-free DFA trades a data-dependent load for an indirect jump and I-cache pressure and does not win universally, and “no branch mispredictions” is backwards for a state machine - the win is where analysis removes states.wasmi was badly reasoned: as an interpreter it is roughly an order of magnitude off a JIT, which removes the very speed advantage that would justify Tier 2 over a plain bytecode VM - and the grammar has to be translated to Wasm before anything runs, which for one evaluation dominates what it was meant to accelerate. The honest claim is conditional: the advantage exists for large workloads with a cached module and is negative otherwise. The workload half is unmeasured, and this session already watched an equally plausible assumption (Cranelift as the dev backend) come back 25 s against 26 s.git pull, where a pull request’s DSL executes natively on every reviewer’s machine and in CI, which is the build.rs attack surface exactly. And two of the three supposed sandbox costs do not track trust at all - determinism is ADR-005 D8’s hard guarantee and your own code is as able to break it as anyone’s, and the fuel budget is a tool for the local developer, since a build that hangs silently is worse than one that stops with a call path. That leaves memory and filesystem isolation as the single cost that genuinely tracks trust, and a direction to consider: declare the capability in the manifest instead of inferring it from a directory, so it is a line a reviewer sees in a diff.comptime cannot do I/O; @embedFile is the separate declarative capability - D1 plus Q2’s restrict branch, shipped), Jai permits, Rust keeps a separate mechanism whose Tier 1 application is served by a library (phf). Rejected with reasons: a raw-pointer host/guest ABI (voids D1 and isolation, and leaks allocation addresses under ASLR - the layout in guest memory meets its legitimate core), 1BRC as evidence for comptime (our own CHANGELOG attributes those gains elsewhere), “faster than ripgrep” as a claim (the architecture permits it; the sentence invites a benchmark ripgrep wins, because its speed is literal prefilters), and “typed AST nodes at compile time” as a present-day advantage (ADR-013: there is no type checker).const - purity, allocation, and a fuel budget, because a compiler a dependency can hang is a supply-chain problem in a performance costume. Then: restrict actions to a pure subset (and need no sandbox) or permit and sandbox them; if sandboxed, wasmi vs wasmtime vs an own VM, weighing a wasm32 target requirement for every contributor and Wasm’s determinism asterisk on NaN bit patterns against ADR-005 D8’s hard byte-determinism guarantee; and whether confinement needs an explicit grant in D’s -J style. On that last one the ADR credits what ADR-021 already built - every read is recorded in the committed lockfile, so a dependency that starts reading your secrets.toml shows up as a diff in review - while saying plainly that this makes the capability visible, not absent, and that the two are not the same claim.bridge_orchestrator::cache implements ADR-021. The crate that every document named as the home of build caching had been fifty lines ending at a TODO; it now holds the thing: SHA256 input hashing, nikaia.lock as the committed record, a cache key per translation unit, and a content-addressed artifact store with no index of its own. An unchanged unit is served from the store instead of being lowered again.--no-cache is the way out (D11). Reusing an unchanged lowering is the difference in feel between a Nikaia build and a Rust one, and a default nobody types is not that difference. Being the default is also what turns where the files go into a requirement rather than a detail: nikaia.toml is found by walking up from the input, as Cargo finds Cargo.toml, and decides the shape. In a project the lock is committed at the root and the store sits in target/nikaia/cache/, with the unit named by its path below the root (src/main.nika) so the key does not move with the checkout. Outside one, a committed lockfile would have nothing to be committed to, so the record joins the store in the user’s cache directory and nothing whatsoever is written into the source tree - asserted by a test that lists the directory afterwards.sync-checked, lowered or inferred on a hit - sound for one reason only: a failing check returns before anything reaches the store, so an entry’s existence is the statement that this input passed, and skipping the check is reading its memoised result rather than not running it. The price is D7’s discipline applied strictly - every input those checks consult has to be a dimension, and a future check that reads something outside the list must add it in the same commit or the cache starts approving inputs nobody checked. The emitted Rust and ADR-020’s ledger now travel in one envelope under one key, because separate keys would allow a half-hit with the Rust present and the ledger missing and nothing to notice; an envelope that lacks an artifact the current build needs reads as a miss, which keeps adding an output a safe change. What a hit does not skip is --locked: whether the committed ledger still matches is the user’s question about the working tree, not the compiler’s about the source, and a test tampers with the file to prove the check still bites on a cached build.CARGO_PKG_VERSION stays 0.1.0 across every edit to the emitter, so keying on it left the key still exactly while the thing it identifies moved: D7’s second failure direction, serving the previous emitter’s output and calling it fresh. It lands only on compiler developers, which is why it would have survived. build.rs now hashes the sources that decide the output - the compiler’s own src/, and the std ledger contracts::STD bakes in with include_str!. A version string is a claim about the compiler; a fingerprint is a measurement of it.Result - it should be able to say what broke - and the caller is what decides it is not fatal. Driven through the real binary in the tests, because that decision lives in main.rs and nowhere else.Key::build, which is the whole of D7 in code: nothing is hashed that is not listed there. Fields are length-prefixed, so ("ab", "c") and ("a", "bc") cannot collide - there is a test for exactly that, because the alternative is a cache that is silently wrong for one pair of inputs in a thousand. The unit is named relative to its root rather than by absolute path, since a key that moves with the checkout never hits.crates/nikaia/tests/cache.rs lowers examples/1brc.nika under both profiles and asserts each comes back as itself - and first asserts that the two lowerings differ at all, so the test cannot quietly become vacuous the way D5 warns the_profiles_agree_on_every_example would if the cache confused them. The lockfile is asserted to contain the unit and its hashes and not the profile: a committed file that a profile switch rewrites carries a diff that means nothing. 15 tests in all, 11 on the key’s dimensions and 4 end to end.--backend cranelift and --backend llvm now fail instead of pretending. Both were accepted by the CLI and silently ran as bridge, because the backend list in main.rs was a comment and nothing matched those strings; an unknown value fell through the same way. The dispatch is a match with named arms, and the error says which backends exist. Implementing the two remains open - ADR-021 D9 records it - but accepting a flag and doing something else no longer is.build.rs records NIKAIA_RUSTC_VERSION beside the NIKAIA_RUSTC it already exported. It is the rustc that built the emitter, which is the one whose output the cache would be serving, and asking for it once costs no subprocess per build.cargo or inject RUSTC_WORKSPACE_WRAPPER (ADR-003), nothing reports assets yet since compile-time I/O is unimplemented, and the cache covers the lowering only - the rustc subprocesses in rustc-executor and the test harness, which is where the measured repetition actually is, are still uncached.nikaia.lock was called a “deterministic cache key” (ADR-002), nikaia.contracts an “incremental cache key” (ADR-005), and the orchestrator the place that does “hash-based incremental compilation checks” (ADR-003) - three descriptions of caching, no statement of what is hashed, what invalidates what, or where anything is stored. None of it exists in code: bridge-orchestrator is fifty lines that end at a TODO, and there is no hash, no lockfile reader and no artifact store anywhere in crates/. The ADR is written before that component, because a cache key is the one thing that cannot be widened later without invalidating everything built under it.sccache was considered and rejected, not because it works badly but because it caches the wrong layer: it memoizes rustc invocations, while the repeated work in a Nikaia build is macro evaluation, grammar expansion and lowering - all of it before rustc is reached and none of it visible to rustc. A cache keyed on inputs we already record covers that work; one bolted underneath it cannot.emit/mod.rs is a pure function from source to Rust, which is what makes caching sound at all, but it is a function we edit. Without it in the key the cache serves artifacts from the previous emitter, and the failure lands only on compiler developers - which is exactly why it would survive undetected. nikaia.toml declares what is required, nikaia.lock records what was used; a constraint and a measurement can coexist where two constraints drift.docs/handoff.md records what the alternative costs, where Cargo fingerprints winnow-grammar on its commit and local edits are silently inert.the_profiles_agree_on_every_example one artifact twice and the test would pass vacuously. A cache may make a test faster; it may never make a test empty.rustc_private/rustc_driver linkage that was the plausible blocker, the full workspace including proc-macro crates, all 51 tests green, identical binary output - and it buys 25s vs 26s on a full build and 1s vs 2s incrementally. At opt-level = 0 the cost is the dependency graph and the proc macros, not codegen. So: a supported, documented option behind [profile.dev] codegen-backend, and not the default, since defaulting would oblige every contributor and CI job to install a toolchain component these numbers cannot justify. The measurements are in the ADR so the revisit is a measurement and not an opinion.nikaia.contracts and nikaia.lock each call themselves a “cache key”, and that shared word is what makes folding one into the other look reasonable. Two properties make it impossible rather than unwise: the ledger ships with published packages (ADR-005 D3) while a library’s lockfile is ignored by its consumers, so one file would have to be both shipped and not shipped; and --locked would mean opposite things - regenerate-and-compare byte for byte for the ledger, which is sound only because it is a pure function of (sources, toolchain), versus do-not-re-resolve for the lock, which is mandatory because resolved dependency versions are not a function of the source tree at all. Two softer reasons point the same way: the ledger is derived, so moving it into the lock adds nothing to reproducibility, and its diff is the diagnostic NK2401 cites by line - a signal that does not survive interleaving with asset hashes that churn on every touched file.toolchain = field in nikaia.contracts is not drift, because a ledger that travels downstream without the lock has to describe itself - otherwise a consumer cannot tell a compiler-upgrade diff from their own. Duplication a file needs to be readable alone is not duplication to remove.--backend cranelift and --backend llvm are accepted by the CLI and silently do what --backend bridge does - the backend list in main.rs is a comment and nothing matches those strings; accepting a flag and quietly doing something else is the worst of the available options and it is the one shipped. And ADR-001 and ADR-002 both still read Active/Accepted although ADR-003’s own header supersedes them; their status lines now say so..route("/") fn: "Hello World" - so a handler could read no query parameter and set no status code. fortunes.nika never noticed, because the fortunes benchmark needs neither; the other six TechEmpower tests need both.fn: "Hello World" keeps working unchanged and for the same reason it worked before, and fn: a.query("name") ?? "world" reads one. Nothing is added to the language, and there is no second implicit argument: a response is returned, not filled in.String is 200 text/plain, an html::Raw is 200 text/html, a Response is itself, and a throws that fails is 500 with a generic body and the error in the log. That last row is a decision rather than an omission - an error’s message is written for the operator, and a handler that hands one to the client is how internal paths and driver messages reach a bug bounty report. html::Raw earning text/html is ADR-017 D2 read from the other end: the type that says “this is already markup” is the type that may be sent as markup, and a String, which might be anything, may not be.Response, built where it is returned - http::Response(status: 400, body: "id is required"), the anonymous-constructor form of Kap 4.2 with no builder chain. The request’s strings are views into the bytes the connection read (ADR-008), so a parameter used inside the request’s scope costs nothing and one kept past it has to be owned; query and header return Part I 3.5’s nullable rather than an empty string, and method returns an enum rather than a string. The surface stops there on purpose: route patterns, cookies and multipart are each a decision of their own and none of them is what G6 was about.current_request() reading thread-local state, which is a lie under a work-stealing executor; the two-argument fn(request, response), which makes “was it already sent?” a question every middle layer has to ask; an always-required Request argument, which would break the specification’s own one-liner; and returning String as HTML, which would make every string in the program a potential injection.fortunes.nika and examples/README.md say so instead of describing a hole.trusted: true upstream must not become an XSS hole downstream in a file that does not mention escaping.html::Raw, not a flag at the hole. A flag is a property of the call site and the same value reaches many call sites; a type travels with the value, is decided where the value is built, and Raw::new is one line to grep for. It is the only constructor, so the audit is a grep rather than a type search.<script> body, a URL, an unquoted attribute and a CSS block each need something else - so a hole in one of those is a compile error naming the position. That is what lets the first decision be unconditional: a promise that held only in some positions would have to be qualified everywhere, which is the “escape unless trusted” answer wearing a different hat.std::html::escape is implemented and tested (crates/nikaia-std/src/html.rs), in std rather than in the code generator so there is one implementation, testable on its own, and a hand-written renderer gets what a template gets. It returns its input unallocated when nothing needs escaping - the common case for a database column - so the contract costs a scan rather than a copy. Two things the tests pin because a reviewer should check them: & is escaped first, since escaping it last would escape the ampersands the other four just produced, and escaping is not idempotent, which is precisely why an already-escaped value gets a type instead of being recognised by inspection.match, which Part I specified and Stage 0 could not hear)enum lowers, in all three shapes Kap 4.4 shows: Quit, Write(String), Move { x: i32, y: i32 }. An enum that carries a view takes the input lifetime exactly as a struct does - the analysis walks a variant’s field types the way it walks a struct’s - which is what lets one appear in a grammar rule’s return type.match lowers, with five pattern shapes: _, a literal, a variant path (Op::Times), a variant with positional bindings (Message::Write(text)), and one with named bindings (Message::Move { x, y }). A bare name binds, which is one rule rather than two: a path with :: names a variant, a name without one binds, and that is the line the enum’s own syntax already draws. Every shape is one the language below spells the same way, so the lowering is a transcription rather than a translation (ADR-011 D2).head_expr, for the reason if’s condition is one: match value { would otherwise read value { … } as a struct literal and take the arms for fields.examples/calc/src/main.nika closes G12 with it. Its two operators were a &str and a comparison; they are an Op and a match now, and mul_tail yields (Op, i64). The AST had carried stub definitions for Item::Enum and Expr::Match since an early draft - both unreachable, one of them mislabelled Kap 4.3 - and they are replaced rather than extended.sort() and sort_by_key, and that both are stable - which closes G5. A map has no order to borrow: ADR-010 D6 seeds an untrusted one randomly, so its iteration order differs between runs, and a program that prints one has to say which. Stability is what makes two passes state a compound order without a comparator: names.sort() then rows.sort_by_key fn: -a.1 is by hits descending, ties in name order. examples/access-log/src/main.nika prints its paths that way, and builds the rows as tuples so the key can be a number without looking anything up.a inside a string)a only inside a string hole was generated with no arguments at all. How many arguments such a lambda takes is read off which of a, b, c its body mentions, and a string literal keeps its text - the holes are parsed when it is emitted, so the scan never saw them. xs.map fn { "{a.0}={a.1}" } came out as map(|| …) and did not compile. The scan now parses the holes as the emitter does.let c = … - is asking for a third argument rather than shadowing anything. Telling a use from a shadowing binding needs scope analysis Stage 0 does not have; where a local wants one of those names, the explicit form names the arguments. examples/access-log/src/main.nika renames a local for exactly this reason, and says so.rule A -> i32 = n:digit1 { n } - an action block whose -> was forgotten - reported expected digits, because the grammar read { n } as a bound and a bound wants a number. That is true of the parser and no help to the reader. The lookahead peek(("{" digit)) says the rule the backend already states for the same ambiguity (SYNTAX.md, “Braces”), and C2 now reports expected `->`.winnow-grammar fd17a8c -> e222ae8 (upstream #10), which is the other half of it: with the lookahead alone, every brace group that was not a bound reported expected a digit. A peek(…) consumes nothing and demands nothing - an alternative whose lookahead says no simply does not apply - so what fails inside one is a test that said no rather than an expectation, and it is no longer recorded. Without that it also won, because a lookahead is tried one token further along than the thing that actually belongs there." an unterminated string is missing, and its position is still the end of the file rather than the opening quote. Remembering the opening delimiter is a separate mechanism and is not done.winnow-grammar f609cea -> fd17a8c (upstream #8), and tests/errors/EXPECTED.txt regenerated. Two changes there, the second because the first needed it. alt threw away what a losing alternative found - right when it failed where it started, wrong when a shorter alternative wins over one that read tokens first; an alternative that had begun is recorded now. Then two requirements met at one position and the wrong one won, because two expectations beat one on priority - the tie-break is how long each has been open, and the attempt that started earlier is the structure the reader is inside.expected one of: `//`, whitespace and were recorded twice as “the whitespace skip wins on progress”. let xs = [1, 2 has no list literal to parse, so let and xs each parse as an expression statement and the alternative that would have said expected expression at the [ is abandoned - leaving the skip as the only error at that offset. All three now read expected expression, at the position the input actually goes wrong." it is missing instead of `\`, any character. Its position is still the end of the file rather than the opening quote, which is a separate mechanism and is written down as such.digits comes from the built-in name table and the rule really is looking at a repetition there. A grammar question, not a message question.calc.nika was writing a struct to stand in for)(A, B) as a type, (a, b) as a value, t.0 to read a part (Part I, 4.5). A pair of values of different types with no name for the group is a tuple, and Stage 0 had no way to write one: a type was a name with optional generic arguments, so examples/calc/src/main.nika declared a two-field struct Step to carry an operator beside its operand - a struct pretending to be a type, and the first place in the examples where Nikaia read worse than the language it lowers to (gap G10).(&str, i64) in a struct field ties that struct to the input exactly as a bare &str does, and grammar_lowering.rs pins it.(a) is still grouping: the comma is what makes a tuple, in Nikaia as in the language it lowers to. calc.nika’s mul_tail yields (&str, i64) and its struct Step is gone. What is still missing there is the enum - two operators are exactly what a sum type says - and that is now gap G12.rule expr -> Expr # "expression" = …, in .nika grammars and in the compiler’s own. A rule with several alternatives fails by reporting what each of them could have started with, which past three or four is useless: expected one of: '!', '-', 'dsl', 'false', 'spawn', 'true' where the word expression belongs. The name replaces the list only where the rule failed at its own starting position - a rule that got further was in the middle of something, and what it was in the middle of is the better message. Spelled as the backend spells it, because the lowering is name for name (ADR-011 D2). Specified in Part II 10.6; examples/calc/src/main.nika labels its factor and crates/nikaia/tests/examples.rs checks the whole chain end to end - a Nikaia grammar names a rule, the emitter hands the name over, the backend reports it, and the driver renders it against the input.expr and unary_expr as expression, stmt as statement, item as item, type_ref as type. unary_expr needs its own label and says why: 1 + fails inside add_tail, whose operand is a mul_expr, so the label on expr never sees it.winnow-grammar e1b0e33 -> f609cea, and tests/errors/EXPECTED.txt regenerated. Two changes upstream, both measured against docs/error-corpus.md: the rule label above (#5), and an element that began is not an optional continuation (#6). The second is the larger one. A repetition at or above its minimum records the element’s error instead of returning it, and that marked the whole of it optional - including what the element had already established. Where the entry rule is a repetition, and program = item* is one, that is everything: an unfinished item’s missing } ranked no higher than the operators a complete expression could have continued with, and lost to them on priority.fn at end of input, seventeen expectations in the headline and twenty-two in the note - become expected `}`. B2, B4 and B5 become expected expression and expected type. A1, A2, C3, C5 and E3 name the one token the parser requires and put the rest in the note. Sixteen of the twenty-one failing rows now say what a reader needs; five do not, in three groups, and docs/error-corpus.md says which and why. The open one is trivia winning on progress (A4, B1, G1): the whitespace skip runs at the start of every rule, and progress is decided before any ranking or label.std call it needed)examples/config/src/main.nika: an INI-style configuration file, parsed and printed back. The first example whose result is a tree rather than a number or a tally, and the first with a grammar that defines its own whitespace: WS = (WSE | COMMENT)* is the rule the backend inserts between the elements of every syntactic rule, so redefining it makes # legal everywhere a blank is without one other rule in the file mentioning comments. It is also the counterpart to 1BRC’s fold - setting* collects, which is right for a configuration file and wrong at a billion rows - and the transitive half of Part II 10.6: Section says no & anywhere and is tied to the input all the same, because it holds a Vec[Setting] and a Setting holds views. until("#" | line_ending) with s.trim() is what makes port = 8080 # the usual one a port rather than a port and an opinion; the price, stated in the file rather than hidden, is that a # inside a value is a comment.std::fs::read_to_string, which that example needed and std did not have. Part III 17.1 specifies it; fs::map was the only file call implemented, because it was the only one 1BRC reaches for. The other half of the same module: map hands back pages it does not own, this copies into a String the caller does.cargo test)examples/calc/src/main.nika: a four-function calculator, and the grammar protocol at its smallest - no @frame, no par_fold, no sync, no I/O, one pub rule that returns a number and is entered once over one string. It is here because 1BRC alone would make the protocol look like a benchmark trick: what this shows instead is recursion (factor reaches expr again through parentheses), precedence as structure (three rules, no precedence table) and left association without left recursion (the tails are collected and folded in the action, which is the standard repair for a rule that cannot say expr "-" term).examples/access-log/src/main.nika: a web server’s access log summarised - request count, failures, and hits and bytes per path. The shape most real work has, and three things neither other example shows: a frame with four until(" " | frame_end) scans in it, each of which has to name the boundary or be rejected (ADR-009 D1); a field whose width the format fixes (digit{3}, so 20 is a parse error rather than a number followed by rubbish); and what a rejected line looks like, which is where G11 below came from.crates/nikaia/tests/examples.rs: every example that claims to run, lowered from the real file, compiled with the rustc that built the test, run, and compared - under both profiles, with the outputs required to be equal. That last part is the claim the profiles rest on (Part I) and it now has a test rather than a paragraph. A fourth test fails on any examples/*.nika that is in neither list: an example is either runnable and says what it prints, or specification-level with its gaps in examples/README.md.dsl … from … beside a catch did not compile. The lowering emitted its own ?, so dsl Log from data catch { … } became match <value> { Ok(..) => .., Err(..) => .. } - a handler matching on something already unwrapped. catch is the one place that wants the Result itself and now gets it; everywhere else the parse still propagates. Pinned in grammar_lowering.rs.{error} in a catch printed expected a digit; found unexpected token ` ` plus the rule stack, and no position: a ParseError carries an offset and not the text it came from, and Display cannot invent one. The driver is the one place holding both, so it now binds the input and renders against it - … at line 2, column 26, and for a par_fold that offset is in the whole file rather than in whichever piece a core was holding (ADR-009 D3). Note what this is not: docs/error-corpus.md’s closing finding, that no message shows the source line with a caret, is about the compiler’s messages for .nika files and still stands.examples/README.md opens with a table of the four programs and which of them run, and gains G10 - no tuple, and no sum type: calc.nika has to carry an operator alongside its operand, a tuple would say it and an enum would say it better, and Stage 0 has neither - so the example declares a two-field struct Step where one line should have done. The first place in these examples where Nikaia reads worse than the language it lowers to, which is why it is written down rather than worked around quietly. Subtraction escapes it by being addition of a negation; division cannot.winnow-grammar 024e3d3 -> e1b0e33 (upstream #4), and tests/errors/EXPECTED.txt regenerated against it. The change is the one docs/upstream/winnow-grammar-findings.md §1 proposed and docs/upstream/0001-expectations-by-requirement.patch implemented: an expectation is ranked by whether the grammar required it - a repetition below its minimum returns its element’s error, one at or above merely records it - and among what it merely accepts, the implicit whitespace skip ranks last. Nothing is discarded; what loses becomes a note: also possible here: … line, whitespace excepted, because the skip is greedy and asking for more of it cannot move the parse. Landed with it upstream: the documentation the patch had not carried, two cargo doc errors that had upstream main red since 54acc33, text(p)/dec<T>(p) inside a #[frame] (findings §3), and findings §2 and §4 as upstream TODO items.expected one of: `,`, `}` with note: also possible here: `//` where it read expected one of: `//`, whitespace; struct S { name &str } reads expected `:`; let x: = 1 reads expected one of: `&`, identifier; a stray } at top level reads expected end of input. docs/error-corpus.md now has before and today columns and holds the nine rows that did not move, in the three groups that each need a different mechanism.fn, at end of input) no longer name trivia, but the headline is seventeen tokens and the note twenty-two more - at end of input every continuation of every open rule is live at one offset, and no ranking shortens a list whose every entry is genuinely possible. That needs a label, which is group 1 of the same three.tests/errors/ + crates/nikaia/tests/errors.rs + crates/nikaia/examples/errors.rs: the 26 corpus cases as files, what each produces today in one checked-in EXPECTED.txt, and a regenerator - the shape tests/samples/ and grammar_lowering.rs already use. The golden is generated against the shipped backend, so the suite is green and the wrong messages are on the record. Applying the pending backend patch makes the test fail on purpose: the diff is the twelve of twenty-one failing rows it moves.docs/upstream/0001-expectations-by-requirement.patch: the winnow-grammar change (542 lines, 7 files, 279 tests passing, based on 024e3d3) preserved in this repository, because the session that wrote it had no push access to that repository and the clone lived in a container. docs/handoff.md says how to apply it, how to test it against Nikaia, and - at some length - which two ideas were tried against this problem and must not be tried again.docs/error-corpus.md: twenty-six broken .nika files, what a reader needs from each, and what the compiler says today - in two columns, one for the shipped backend and one for the expectation-ranking change that is written and tested but not pushed. Written before any further work on messages, because three typos measured by hand is not a corpus and an intermediate attempt at the ranking made one case much worse (17 expectations in the headline, 22 in a note, the position on a token that was correct) and was caught by accident rather than by a test. The pending change moves 12 of the 21 failing rows; the 9 that remain sort into exactly three groups, each needing a different mechanism - a label where a list of token spellings should be one word (expected expression), trivia still winning on progress because the whitespace skip runs at the start of every rule, and two rows where the position is wrong rather than the text. Found on the way, by trying to break the compiler on purpose: if { }, let 5 = x and if a = b { } all parse. Also recorded: no parse error shows the source line at all, where a rustc diagnostic routed through --explain gets a snippet and a caret - an asymmetry that applies to all 26 rows._sp:skip_ws bindings are gone, and with them the skip_ws rule. They were redundant - the backend inserts WS between the tokens of every syntactic rule, and every one of Nikaia’s own rules is syntactic - so 92 lines of grammar say exactly what 153 said. The 26 tests and the sample corpus are what proves it: nothing about what parses changed.skip_ws calls WS as an ordinary rule, past the marking the code generator puts on the whitespace it inserts itself, so the backend cannot tell that failure apart from one the grammar cares about. With the calls gone and the backend change beside it, a missing comma reads expected one of: ',', '}' with note: also possible here: '//' where it read expected one of: '//', whitespace.docs/upstream/winnow-grammar-findings.md, with a reproduction for each. (1) Two expectations beat one and the one is discarded. Every Nikaia syntax error names whitespace instead of the token that would fix it (expected one of: '//', whitespace where , is meant), because WS = (WSE | COMMENT)* fails with two expectations and is raised to PRIO_AGGREGATED, while the real expectation has one and is thrown away at the same offset. It is not about comments: a grammar with no custom WS at all (xs:item* "." over 1 2 x) drops the . the same way, so any rule with two or more alternatives suppresses every single-expectation error at its position. Proposed shape, checked against tests/diagnostics.rs p06-p09 and leaving all four unaffected: union the expectations at equal offset instead of returning one side (with fail(…) and labels staying authoritative), and rank by requirement rather than by how many alternatives an error happens to carry. The criterion is that an expectation is worth reporting only if satisfying it can move the parse past this position - which whitespace provably cannot, the skip being greedy and already exhausted there. Its mechanical form already exists in repeat_recording_bounded: an error returned below the minimum is a requirement, one merely recorded at or above it is an optional continuation. So requirements make the headline, optional continuations a note: also possible: …, and optional whitespace is dropped - with no classification, no declaration and no list of rule names. x+ below its minimum returns, so nothing suppresses a repetition. (2) The word-at-a-time class scan loses below ~6-8 characters: out_mask is a fixed ~30 instructions per call against ~4-5 per character, so 1BRC’s digit{1,2} cannot come out ahead. Measured with branches and caches simulated, not instructions alone - the scan does buy 14 067 fewer mispredicts (~0.28 M cycles) for 12.7 M more instructions (~3.2 M cycles at IPC 4), about elevenfold against. A threshold in the code generator, not a change to undo. (3) dec(p) and text(p) cannot appear inside a #[frame], where intern(p) is already excepted with the reasoning that fits all three. (4) The intern cache’s 512 direct-mapped slots are sized for identifiers; at 413 keys it costs 617 instructions per row against 514 for a plain fast-hashed map.fs::map is a trusted source, so 1BRC’s station map should get a fast hash with nothing written in the source about hashing - and Stage 0 cannot do it, because choosing the hasher means knowing that a map’s keys are views into a trusted buffer, which is dataflow through the grammar’s return types and a struct field, and ADR-011 D2 forbids the emitter from inferring one. Stage 0 therefore treats every buffer as untrusted (RandomState is ADR-010 D5’s Untrusted row), which is ADR-010 D1’s own “barriers widen to Untrusted” applied to the widest barrier there is. Measured, 8M rows with 413 stations, byte-identical output: --profile lite 0.53 s / 656 instructions per row against 0.46 s / 514 with the fast hash; --profile advanced 0.15 s against 0.12 s. 22 % of the flagship’s instructions, and the largest single item left - the map is 39 % of all instructions, SipHash alone 27 %. No .nika file gains an annotation: ADR-010 §4 rejects both the constructor the user must remember and a global default in either direction. Also recorded there: the intern-cache route was measured and rejected (617 instructions per row at 413 keys against 514, because the cache is 512 direct-mapped slots with no collision handling - right for a compiler’s identifier stream, wrong for an aggregation), and D5’s “the build fails without a real entropy source” is not true yet.dec[i32](digit{1,2}), intern[Symbol](...). Nikaia spells a type argument with brackets everywhere (Kap 4.3), which is what keeps < free in the language; the backend spells it with angles, and the emitter is the one place the two meet, so no .nika file writes the backend’s spelling. Before this, dec<T> was simply unreachable from Nikaia - the grammar parser stopped at the <.dec[i32](digit{1,2}) in 1BRC’s TENTHS costs 693 instructions per row against 659 for the two-iteration value * 10 + digit_value(d) loop it would replace (0.55 s against 0.52 s on 8M rows). str::parse does its own scanning, sign handling and overflow check; over one or two digits that is more work than the loop, not less. dec earns its place on a wider field, not on this one. Also found on the way: the backend’s frame check treats dec(p) and text(p) as consuming anything, so neither can appear inside a #[frame] - intern(p) is already excepted with exactly the right reasoning (“it is a map over its argument, and the argument is checked on its own”), and these two are the same case. Reported upstream; @frame(..., unchecked) is what made the measurement above possible.fs::map cuts the mapping into one piece per core and validates the pieces in parallel; a cut walks forward off a continuation byte so no sequence is split, and the reported offset is the earliest failure, so a rejected file names the same byte however many cores looked at it (four tests hold this to std::str::from_utf8 as the oracle, including a corruption at every cut +-8 bytes). Below 1 MiB it stays in one piece. D3: the profile does not reach into std and does not here - ADR-011 D4 lets nothing but the DSL driver read it, and a check with the same verdict and the same offset on one core and on sixty-four has no result to merge, so Lite is sequential parsing, not a vow of single-threadedness. Measured on 8M rows, 4 cores: lite 0.57 s -> 0.52 s, advanced 0.19 s -> 0.14 s; the check alone 63.7 ms -> 16.5 ms (1.7 -> 6.6 GB/s), ~7.6 s -> ~2.0 s projected onto 1BRC’s 13 GB. Instructions per row unchanged at 659 - chunking moves work, it does not remove any; the check is 30 Ir/row, 4.5 % of the instructions and 11 % of the time, because it is a second pass over 105 MB rather than arithmetic. Deleting it outright buys 10 ms and prints a station name that is a &str and is not UTF-8, so it stays (D1). Also recorded: std::str::from_utf8 runs at 10.3 GB/s on an ASCII-only twin of the file and 1.7 GB/s on the real one, because one non-ASCII byte drops its word-at-a-time fast path and 1BRC has a multi-byte character every ~68 bytes.winnow-grammar now parses with a zero-sized error type first and only diagnoses a failed parse, replaying from the item a par_fold stopped in (upstream ADR 17); Nikaia takes the default (Diagnose::Replay) and emits no choice of its own - a --profile that picked one would change what a program means, which ADR-011 D4 forbids. D2, the three migrations: parse_<rule>_pieces takes &ParseContext rather than a factory (the factory form is parse_<rule>_pieces_with); a repetition of a character class now yields the text it matched, so digit{1,2} is &str and the corpus says for d in whole.chars(); and a rule binding may not be called input, because the generated closure’s parameter has that name and every action now ends with let _state = &mut input.state; - Nikaia’s own dsl_from_expr shadowed it and binds source now. D3, re-measured on the same machine and the same 8 million rows: --profile lite 3.99 s -> 0.57 s, --profile advanced 1.05 s -> 0.19 s, 4506 instructions per row -> 659 - against 840 for the same aggregation written naively in Rust and 688 for it hand-tuned, so a generated parser is now below hand-written code on instructions per row. The decisive experiment is gone with it: an all-negative file used to parse in half the time of a mixed one (one failing optional per row cost 52 %) and is now the same speed per byte.std - and the Two Claims 1BRC Now Keeps”. Closes ADR-013 §4’s first two items and opens the roadmap’s last line. D1: parts of crates/nikaia-std are .nika files compiled by Stage 0 when the crate is built (build.rs → OUT_DIR → include!); the first is src/text.nika with digit_value, called by 1BRC for every digit it parses. The mechanism is the point, not the size - what a std file may contain is exactly what the compiler can lower, and D1 says which file moves when (list.rs with traits and generics; fs.rs never, being unsafe around a mapping). To make the compiler a build dependency without rustc_private, nikaia gained a rustc-backend feature (default on) owning the binary and the Bridge IR executor. D2: fs::map is a memory mapping (memmap2, read-only, UTF-8 validated once; an empty file is an empty input). D3: the parallel driver runs the pieces in parallel - winnow-grammar’s rayon feature, enabled on the workspace dependency so that one build holds one copy of the crate. Measured on 8 million lines, 4 cores, release: --profile lite 3.99 s wall / 3.99 s user, --profile advanced 1.05 s wall / 3.90 s user, identical output - ADR-009 D2’s “chunk count does not change results”, measured. D4: the tests find which rlibs belong together by reading crate metadata (rustc -Z ls) from nikaia_std outward, because the build-dependency graph leaves two winnow copies in the deps directory and a timestamp is a coin toss.return that is the last statement is emitted as the block’s value; this also fixes throws functions with a return type, whose tail was wrapped twice.1brc.nika’s header says what it now does rather than what it does not.std, Methods, and What Stage 0 Refuses to Infer”. examples/1brc.nika has been the project’s flagship since 0.0.7 and has never run; it does now, and crates/nikaia/tests/one_brc.rs checks that by lowering the real file, compiling it with the same rustc that built the test, running the binary and comparing its output. Decisions D1-D7: Nikaia’s std is a crate (crates/nikaia-std) rather than a substitution table inside the emitter, so what fs::map guarantees is a doc comment on a function instead of a string in a printer; the fold adapter is a lookup in the program’s own impl declarations, which closes what ADR-011 D2 deferred - record is declared &mut self and returns nothing, so the accumulator is threaded, and a method that returns a new accumulator is left as written; throws becomes Result<T, Box<dyn Error>>, catch a match binding error, ?? an unwrap_or_else; Kap 4.2’s anonymous constructor becomes new, with Type(positional) calling it and Type(named: …) building the struct; an implicit lambda takes the longest prefix of a, b, c its body uses; an interpolated hole may carry a format spec after the first colon that is not inside a call or an index; and where Stage 0 cannot infer, the source says it - every edit to the example is listed in D7 with its reason.impl blocks and methods (&self, &mut self, self), pub on functions, the anonymous constructor, throws, return, compound assignment, casts, indexing, float literals, ??, catch, tuple bindings in for, trailing lambdas (fn: e and fn { … }), and Type(field: value) constructors.println/eprintln as macros, and collect() as collect::<Vec<_>>().crates/nikaia-std (new): fs::map, cli::args, HashMap, digit_value and List::map. Nothing is stubbed - fs::map reads rather than maps, and says so where a reader would look.1brc.nika’s note now says what runs and what does not (ADR-013 §4: fs::map reads, and the pieces of the parallel parse run in sequence because the rayon feature is not enabled). Its ten explicit-where-Stage-0-cannot-infer edits are tabulated in ADR-013 D7.nikaia --explain rebuilds it rather than keeping an artifact in step); positions are corrected, messages are not, which is ADR-011 D2 paying out - NAME and MEASUREMENT in the backend’s message already are the user’s rule names, and translating the text would be a second compiler; the innermost span wins and the granularity is stated (items, grammar rules, every pattern, statements - expressions not yet, so a type error blames its statement); an unmapped diagnostic says so rather than guessing a plausible line; spans come from the parser backend’s @= injection rather than an offset scheme of Nikaia’s own; and Nikaia is a filter over --error-format=json, not a build system.--explain reads rustc’s JSON on stdin and reports it against the .nika source. cargo build --message-format=json | nikaia --input x.nika --explain.Span and Spanned<T>; items, statements, grammar rules and grammar patterns carry the byte range they were parsed from. The Bridge IR’s span fields stop being 0..0 for functions and lets.crates/nikaia/tests/diagnostics.rs runs the pipeline rather than simulating it - it lowers fixtures/broken_frame.nika (an until(";") under a @frame(boundary: "\n")), compiles the emitted Rust with the same rustc that built the test, and asserts that the frame check - which runs inside a proc macro during macro expansion - comes back reported at broken_frame.nika:15:11, on the pattern, with the source line and a caret under it.par_fold and the piece driver since ADR-009; Nikaia could not reach them, because a grammar item was parsed as content: String - a shape from which no frame can be checked, no entry rule found and no driver generated. Decisions D1-D8: a grammar is parsed, not carried as text; the lowering is syntactic and never guesses a meaning, so a &mut self method used as a monoid’s merge fails to compile rather than being adapted by an emitter inferring mutation from the shape of a tail expression (that is a typing question, and it waits for impl lowering); @frame lowers key for key with its boundary escape intact and Nikaia does not re-implement the check - two implementations of “checked, never rewritten” would drift, and the disagreement that matters is the one where Nikaia accepts what the backend rejects; the profile chooses the Parallelism and nothing else (Advanced Auto, Lite Off, one token apart), because a profile is a runtime and not a dialect; a rule that is a par_fold gets its binding from the compiler, since ADR-009 D2 leaves an action nothing to add; a view takes the input’s lifetime and the struct holding it comes along, transitively (ADR-008 - the source never writes a lifetime); comments become whitespace through a WS override and string literals get a lexical rule of their own, the built-in’s escape set being one short of the "\n" every frame is written with; and a grammar on the Bridge IR path is an error naming --backend rust, never a silently dropped item.--backend rust (the Stage 0 transpiler) and --profile lite|advanced. grammar Name { … } emits grammar! { … }, @frame(boundary: "\n") emits #[frame(boundary = "\n")], and dsl Name from data emits Name::parse_<rule>_pieces(data, ParseContext::<()>::default, Parallelism::…) - or the sequential driver where the entry rule is not a par_fold.=>, bounded repetition p{n,m}, until(… | frame_end), fold/par_fold, the keyed @frame), struct and use items, @borrowed, and the expression grammar action blocks are made of - paths, method calls, field access, struct literals, fn(a, b) { … } lambdas, unary and binary operators with precedence, ?, for, assignment. if/for heads parse with a brace-free expression rule, so the brace that follows is the body and never a struct literal, which is the restriction Rust puts on the same position.crates/nikaia/tests/grammar_lowering.rs. The checked-in expected output is include!d, so the parser backend has to accept every line the emitter produced, and the generated driver is run at five cuts (Off, 1, 3, 64 pieces, Auto) to check that the answer does not depend on how the input was divided - on an input it accepts and on one it rejects. A further test takes the grammar block out of examples/1brc.nika itself, so the example and the compiler cannot drift apart.1brc.nika’s note and examples/README.md’s introduction now say which half of that file lowers and which does not.frame_end is the built-in that names the enclosing frame’s boundary (written once in the attribute, referenced; resolved statically, an error outside a frame or under two boundaries); until takes an alternation, up to three fixed alternatives in one scan; recover under a frame is rejected with the frame-local alternative as the hint. The attribute is keyed — @frame, @frame(boundary: "\n"), @frame(boundary: "\n", unchecked); the positional @frame("\n") is withdrawn — and unchecked is adopted as the greppable escape hatch, replacing “no unchecked escape hatch in this version”, because the open flank is real: quoted fields, start patterns, escapes and custom scanners are cut points a byte-string boundary cannot find, and each is a future key. Lexical frames and “a frame ends in its boundary” stay; “bounded wherever used” is withdrawn.par_fold rule’s parser skips no whitespace at its entry, so pieces and the sequential parse agree on every input including rejections — “chunk count does not change results” is now a property of accept and reject. Lite’s degradation is the driver’s Parallelism::Off, defined identical to the plain parser.parse_<RULE>_pieces(input, new_context, Parallelism::{Off, Pieces(n), Auto}), optional rayon, error offsets shifted into the whole input; what remains Nikaia’s is choosing how from the profile, the sync check, and the executor (the cut and merge stay public for Nikaia’s own pool). dsl Measurements from data lowers to parse_file_pieces.frame_end, two verdicts, never rewritten, unchecked and the formats it is for, no whitespace at par_fold’s entry.1brc.nika — NAME = until(";" | frame_end), @frame(boundary: "\n") with frame_end as the terminator; examples/README.md G4 updated.winnow-grammar advanced to the main that carries frames (27bbb92).NAME = until(";") consumes anything up to the semicolon, a newline included. The implemented check sorts consumers into three classes — safe, bounded (until and recover’s skip stop at the terminator or the boundary, one memmem2 scan, so a name with a stray newline fails at the newline instead of joining two frames), and rejected (a literal containing the boundary, any, multispace0, a syntactic rule). Three consequences are normative: frame rules and what they reach are lexical; a rule reached from a frame is bounded wherever it is used; a frame must end in its boundary. D3 records what lives upstream (frames_<RULE>, merge_<RULE>, no thread dependency) and what is Nikaia’s driver. D5 records that bounded repetition and the single-digit digit terminal now exist.MEASUREMENT uppercase.1brc.nika — MEASUREMENT is lexical, TENTHS uses digit{1,2} and the DIGITS/DIGIT helper rules are gone, NAME’s comment explains the bounding; examples/README.md moves G9 to Resolved and updates G4 with what the backend now provides.winnow-grammar advanced from f4955bc to current main — bounded repetition, the digit terminal, and the word-at-a-time scan behind until/recover (which now yields the skipped text).Trusted ⊑ Untrusted) on sources, travelling the provenance edges ADR-008 already tracks, recorded in the Borrow Contract Ledger, solved by the same monotone fixpoint (so ADR-005 D8’s determinism covers it unchanged) and failing safe at dyn/FFI barriers — the opposite direction from ADR-008 D7’s representation barrier, deliberately; std classifies its own sources by channel (network, IPC and database rows untrusted — stored data is attacker-chosen data with a delay; files, argv, env and compile-time data trusted); the user has the last word at the place data enters, in both directions (fs::map(path; trusted: false)), with every assertion recorded in one auditable file; a grammar may pin an @untrusted floor its callers cannot lower, which is the DSL’s answer, while no @trusted floor exists because a format cannot make its input safe; the compiler then picks only the hasher — keyed with a per-process random seed for untrusted keys, fast for trusted ones, with the build failing rather than falling back to a fixed seed where there is no entropy source — and never weakens equality or layout; map iteration order is consequently unspecified and may vary between runs; nikaia explain --trust makes every choice visible. D8 keeps the mechanism general (G7’s HTML escaping is the obvious second consumer) while recording that escaping at a template hole must stay unconditional: provenance may add lints there, never switch escaping off.std::collections section — the attack, the two provenances, the inheritance and join rules, the user override at the source, the grammar floor, and nikaia explain --trust.examples/README.md moves G8 to Resolved; G5 (ordered iteration) notes that random seeding makes the explicit ordered form the only correct answer, and G7 records ADR-010 D8’s boundary condition.@frame marks a rule as a resynchronization unit and the compiler verifies the property (the boundary must not be consumable inside a frame, so CSV with quoted newlines is rejected instead of silently miscounted) rather than trusting an assertion; par_fold(rule, init, step, merge) supplies the second half, and parallel parsing is exactly a frame plus a monoid; the lowering (blind split, seam repair, frame ownership, EOF and oversized-frame edge cases, absolute error offsets) is normative; byte scanning is a specified complexity — never one byte at a time — with portable word-at-a-time SWAR as the baseline and SIMD only as a target-gated layer, so Lite and wasm32 keep the same performance story; fixed-width numeric parsing requires bounded repetition in the grammar rather than compiler guesswork; a view may be hashed from its first bytes but equality stays full-content; a large read-only mapping may be left to the OS at process exit. Parallelism is opt-in: a plain fold is never parallelised behind the user’s back, because a merge over floats would make the result depend on the core count. The frame marker is not spelled sync — that keyword already means “pure CPU, cannot pause” (12.1) and names recover’s resynchronization token.@frame, its verification, par_fold, why parallelism must be asked for, and the Lite degradation to a sequential fold.fs::map notes that a read-only mapping is left to the OS at process exit (ADR-009 D7).1brc.nika marks measurement @frame and folds with par_fold; main loses split_aligned, the par_iter/collect and the merge loop, and is now let totals = dsl Measurements from data. examples/README.md moves G4 to Resolved and records two new open gaps: G8 (the default hasher — a DoS-resistance-versus-throughput decision that cannot be made per profile without reopening ADR-005 D8c, deferred to its own ADR) and G9 (bounded repetition p{n,m}, an upstream winnow-grammar request; the pinned f4955bc has *, +, ?, count, fold and no {n,m}).&str in a field is a view marker, not a lifetime — options “one String type with silent copies” and “one String type with a hard error” both rejected as the default rule); a three-state lattice Borrowed ⊑ Tethered ⊑ Owned solved per construction site; uniform state per struct instance, so a struct has at most three layouts and monomorphization is bounded by the lattice height; the Shared handle sits on the container, not on each slice; promotion to Owned is never automatic, and the residual “buffer cannot be shared” case is a hard error with a fix-it; @borrowed as an opt-in compile-time assertion for hot structs; the state joins the Borrow Contract Ledger and inherits ADR-005 D8’s determinism, with dyn/API boundaries as state barriers that widen to Tethered (never to Owned); buffer retention as a named lint rather than a silent leak; explicit region binding rejected as the core mechanism.1brc.nika means a Shared handle on a struct built ~10^9 times and, under the Advanced profile, a billion atomic increment/decrement pairs on one refcount word contended by every worker. Storing a slice in a struct is not by itself an escape. The section gains the state table, the container-level handle rule, @borrowed, nikaia explain --tethers, and the honest cost (a tether pins the whole buffer).fs::map documents retention — a small extract escaping a large map pins the whole map, warned about with a .to_owned() fix-it, and the mapping may be released later than the block that created it.1brc.nika marks Reading @borrowed and explains why both the struct field and the HashMap key stay plain references; examples/README.md moves G3 to Resolved. G4 (line-aligned chunking) remains open.main by c6ff1fa. Decisions D1-D7: scannerless parsing as a language guarantee; } eod explicit DSL termination enabling parallel parsing of the rest of the file; dsl as an expression unifying compile-time and runtime parsing; hybrid binding (meta::capture vs. meta::parameter); shadow types with the typed spread ...args: Self::dsl; hardware instructions moved out of the core; auto-AST and regex terminals explicitly deferred. Records what winnow-grammar already validates (cut operator =>, recover, scannerless input) and the rejected alternatives.=>), lexical (UPPERCASE) vs. syntactic (lowercase) rules, typed actions; auto-AST marked as not-yet-available.dsl G from … in const vs. let), replacing from … with … and the generated P::parse().} eod, scannerless delegation, hybrid binding table, Subject ; Config for DSL parameters, worked SQL example with the typed spread. Names the immediate/deferred split as a recurring language principle (lambda capture 5.4, DSL binding 10.5, cleanup ADR-006 D3).unsafe asm and its register-constraint table removed from the core because WASM is a stack machine with no registers to constrain; dsl backend::x86 / dsl backend::wasm instead; unsafe kept reserved for future FFI.Symbols for identifiers and keywords (cheap equality, dedup), tethered slices for bulk text (string literals, comments, DSL bodies). Adds an explicit correction: drafts up to 0.0.6 claimed the borrow checker ensures stored tokens stay valid; it would reject them, which is what tethering exists to solve.recover(rule, sync) with a commit point, matching the parser backend.dsl js uses a parameter hole (:msg + script.exec(; msg: …)) instead of {message} string interpolation; dsl sql block terminated with } eod.winnow-grammar (a separate repository, pinned by git revision), not syn-grammar. Records why syn-grammar was the wrong tool (it parses a Rust TokenStream, so it inherits Rust’s lexer and cannot host foreign DSL syntax), the backend features this architecture depends on, what the backend does not provide, and the version-pinning constraint.nikaia binary failed to link (cannot satisfy dependencies so 'std' only shows up once, 19 errors) because rustc-executor pulls in the compiler’s dylibs while the binary linked the ordinary rlib std. The binary now declares #![feature(rustc_private)] + extern crate rustc_driver, and a build script adds the toolchain’s lib directory to the rpath so the linked binary starts.winnow-grammar advanced from 43aae1f3 to b03a1f0f and winnow from 0.6 to 0.7. Adapted to the new API: parse_<rule>() is a parser factory driven with .parse_next(), and it runs on a Stateful stream carrying ParseContext. Parse errors are now winnow_grammar::ParseError and are rendered with line/column and rule stack.raw_ident terminal and the AST stores String; syn::Ident/proc-macro2 are gone from the front-end (they were left over from the syn-grammar era and cannot be built from the new terminals anyway). Interning waits on an upstream Symbol off-by-one — see ADR-007, D7.crates/ restructure dropped although the changelog claimed them — spawn_expr (so spawn({ … }) becomes Expr::Spawn instead of a generic call) and block in expression position.tests/hello_world.rs referenced a crate (nikaia_driver) that no longer exists and sat outside any package, so cargo never built it. crates/nikaia gained a library target and the test now runs as crates/nikaia/tests/parser.rs; a new samples.rs asserts that every tests/samples/*.nika parses.let_assignment.nika used a top-level let, which neither the grammar nor the spec allows; it is now a valid program.rust-toolchain.toml adds rustfmt and clippy — without rustfmt the repo’s own scripts/pre-commit.sh could not run.stable while rust-toolchain.toml pins a nightly, and its final step ran bare cargo run --release against a virtual workspace manifest, which only prints clap’s usage error and exits non-zero. It now installs the pinned toolchain via rustup show (single source of truth), caches cargo, and runs fmt check, build, tests, and the driver over every sample.NikaiaFrontend::backend field; added a trace feature forwarding to winnow-grammar/trace so the grammar! expansion’s cfg check is legitimate. The workspace now builds warning-free.build.rs (the workspace root has no [package], so cargo never ran it).winnow-grammar advanced to f4955bc, which carries two changes made upstream for this work — the Symbol round-trip fix (its handle was off by one through lasso::Spur, so resolve returned the neighbouring symbol’s text or panicked on the last one) and the new fold(rule, init, step) repetition.ident, the AST stores winnow_grammar::Symbol, and the interner travels with the AST as parser::Parsed so names can be resolved. This is Spec Part II 10.6 in practice — each distinct spelling stored once, equality an integer comparison. syn::Ident and proc-macro2 stay gone. One cost worth knowing: the derived Debug prints Symbol(3) rather than the name, so Parsed::text is the way to read one.std::fs now has a specified surface instead of a description. Whole-file (read returning a shared Bytes buffer so slices can be tethered to it, read_to_string, write), streaming (lines, bytes — constant memory, immediate contexts when iterated), handles (open → File, which implements Cleanup so a failed flush surfaces as an error rather than being swallowed, plus explicit close() as the opt-in for handling it at a precise point), memory mapping (map), metadata and directory operations, and a per-profile availability table. Every function throws: a missing file is an environmental error, not a bug.fs::map’s availability is a property of the target, not of the profile. Memory mapping is an operating-system service, so it works under both profiles on any target that provides it — a Lite program on Linux maps files exactly like an Advanced one. Only wasm32-* rules it out, where it is a compile-time error rather than a silent fallback to read, because degrading a map into a full read turns a constant-memory program into one that allocates its entire input. This is the opposite axis from std::thread, which is barred by the profile.examples/: specification-level programs that the bootstrap parser cannot compile yet, kept separate from tests/samples/ (which must parse and is covered by cargo test).
1brc.nika — the One Billion Row Challenge: a scannerless grammar for the whole file (not for one line — you write down what the input looks like and get a parser that drives itself, paying the DSL protocol once rather than a billion times), with a commit point after the separator, station names as tethered slices into a memory-mapped buffer, integer-tenths aggregation, and one parser per chunk distributed with par_iter. Chosen because it stresses precisely what ADR-007 asserts — if it is slow, the grammar protocol’s performance argument is wrong.fortunes.nika — the TechEmpower fortunes benchmark: one request handler using both binding modes from ADR-007 D4, deferred parameters for the SQL DSL and immediate capture for the HTML template DSL, over implicit-async I/O in the Lite profile.README.md — survey of the benchmark suites worth targeting (1BRC, Computer Language Benchmarks Game, TechEmpower, Are We Fast Yet?), with a recommended order, plus the specification gaps writing the two programs exposed. G1 (std::fs) is resolved above. G2 was restated and then resolved: the original wording claimed a gap around “applying a grammar per line”, which misread the language — you write down what the file looks like and get a parser. The real gap was narrower, a folding repetition at the entry rule; fold now exists in winnow-grammar and 1brc.nika uses it.docs/winnow-grammar/: a stale copy of another project’s documentation. The one Nikaia-relevant part — the Symbol off-by-one that blocks interning — now sits with the decision it constrains, in ADR-007 D7.docs/analysis/branch-review-0.0.5-v0.0.6.md — forensic review of the 0.0.5 and v0.0.6 branches: both were squash-merged intact, v0.0.6 was then reverted by c6ff1fa, and the resulting version-number collision. Lists the lost ideas, the convergences between the branches, and the conflicts.NK2301 example.nikaia.contracts) — generated committed file serving as incremental cache and as the diff basis for narrated “what changed and what broke” errors (NK2401).--locked verification mode for nikaia.contracts.Cleanup trait (pausable cleanup() throws + synchronous drop fallback, compiler-inserted at scope exit); three-death-paths taxonomy; cleanup errors throw normally (signatures tell the truth, secondary-error attachment during unwinding, explicit close() opt-in); cancellation parks cleanups with the runtime; shutdown drain phase with cleanup-deadline and a termination argument (cannot hang, no deadlock via ADR-005 D6); rejected alternatives (blocking drop, fire-and-forget, explicit-close-only, linear types, rustc async_drop).Drop vs. Cleanup distinction with worked NK2601 diagnostic, sync-context restriction (NK2602), parked-cleanup and honest Lite-panic notes.cleanup-deadline manifest key (13.3); NK26xx resource-cleanup codes in Appendix C.std::panic::on_panic) — global per application, sync, runs on every panic path including Lite’s abort and the WASM trap (rides on the backend invoking the hook before abort under panic = abort); diagnosis-not-cleanup rule, brief-blocking allowance with Advanced nuance, recursion guard; NK2604; stackable hook chains rejected. Spec: Part I 7.2 (worked example), 6.4 note updated, Part III Appendix A.bytes::Bytes model over Shared); buffer provably outlives tokens instead of borrow-checker rejection. Unsafe self-referential codegen recorded as future optimization note.access/access_all require a sync lambda in both profiles (NK2201); runtime reentrancy check/poisoning demoted to backstop.sync (NK2102), async children use spawn + implicit move. Beginner-oriented rewrite with worked error messages.spawn uses implicit move (no move keyword); rewritten with .clone() guidance and worked NK2101 error.nila code-fence typo (Part I, 5.4) and an unclosed code fence (Part III, Appendix B).expr rule to include block, enabling parsing of blocks in expression positions (e.g., spawn({ ... })).spawn_expr rule to correctly parse spawn statements as Expr::Spawn instead of generic function calls.skip_ws rule to consume trailing whitespace at the end of the program, preventing ParseError at EOF.ws to skip_ws to avoid infinite recursion bug in winnow-grammar.tests/hello_world.rs.src/main.rs and tests/hello_world.rs to wrap input source in LocatingSlice to satisfy winnow::stream::Location trait bounds required by the generated parser.