Renyi Design Decisions, Round 1
Status: accepted. Date: 2026-10-05.
This record captures the first round of design decisions for Renyi. Each entry lists the question, the options that were on the table, the decision, and the reason. Decisions marked (user) were made explicitly by the language owner; decisions marked (derived) follow from those answers and the design reasoning recorded under them. The questions were asked in an interactive session; the original question list lives in git history (commit 231567e).
Later rounds append new entries rather than editing these. A decision is reversed by a new entry that names the one it supersedes.
0. Tensions and the trade-offs taken
The requirements pull against each other in four places. These trade-offs were put to the owner and accepted.
| # | Tension | Decision |
|---|---|---|
| T1 | English-like syntax vs. token cost and LLM "plausible invention" | Regular English, not natural English. Fixed clause shapes, exactly one spelling per concept, no synonyms, one formatter output. (user) |
| T2 | Scripting convenience vs. Haskell-grade strictness | No dynamic escape hatch. Full type inference inside function bodies; mandatory signatures at the top level; errors carry fix suggestions so an agent self-corrects in one loop. (user) |
| T3 | Interop with other languages vs. strictness | FFI is first-class but boundary-validated. Foreign bindings read like ordinary functions and are generated from the host's own type information; every value crossing the boundary is validated at runtime and failures flow through the normal error channel. (user, revising the originally proposed "second-class but safe") |
| T4 | Runtime speed vs. compile speed vs. implementation effort | Compile/check speed > implementation effort > runtime speed. v1 is a bytecode VM; hot paths get AOT later. (user) |
A. Positioning
A1. Script language, application language, or both. One toolchain, two
modes. renyi run file.ry type-checks and executes on the bytecode VM with
sub-30 ms startup. renyi build produces a self-contained executable (VM plus
bytecode). AOT compilation is a later milestone. (user)
A2. First real use cases. All four: agent workflows and API glue (including a sandbox for agents to run computations safely), data processing and cleaning scripts, backend services, and CLI / automation tools. The standard library and the capability model are prioritized in that order. (user)
A3. Primary author. LLM agents write, humans review. Consequences: errors are machine-parsable, the grammar forbids alternative spellings, documentation clauses are mandatory on public definitions, and the acceptance test for every syntax decision is "a human reviewer understands it at a glance". (user)
A4. Performance anchor. Lua-class interpreter for v1: bytecode VM, startup under 30 ms, several times faster than CPython on typical scripts. No AOT or JIT in v1. (user)
A5. First-class platforms. Linux, macOS and Windows command line; server-side WASI; browser WASM; embedding into host programs (Rust, Python, JS, C) through a stable C API. Mobile is out of scope. (user)
B. Types and strictness
B1. Effects in the type system. Yes, fine-grained. A function declares the
capabilities it uses with a needs clause: needs network.http,
filesystem.read. Time and randomness are capabilities too, so a function with
no needs clause is pure and therefore replayable and trivially testable. The
checker requires every caller to declare a superset of its callees' effects.
Effects of function-typed parameters flow to the call site automatically, so
users never see effect variables. (user)
B2. Error handling. No exceptions. A function that can fail declares
or fails with ErrorType in its signature; the caller writes otherwise fail
to propagate, otherwise fail with NewError to translate, or otherwise
<default> to recover. Result and Maybe exist in the compiler but users
rarely spell them. The only ways a program terminates abnormally are an
explicit crash with "message" and arithmetic domain errors such as division
by zero. (user; the arithmetic exception is derived)
B3. Mutability. Immutable by default. let mutable name be value opens a
local mutable binding; set name to value updates it. Mutable state cannot be
shared across functions; the type system has no reference type. Imperative
for each loops stay in the language because LLMs write them more reliably
than folds. (user)
B4. Evaluation. Strict. A lazy keyword is reserved for opt-in laziness in
a later version. (user)
B5. Type system features in v1. Algebraic data types with exhaustive
pattern matching; generics spelled List of Item; abilities (type classes
without exposed higher-kinded types); refinement types and contracts spelled
Integer where value is at least 0. Higher-kinded types, GADTs and dependent
types are not implemented even internally. (user, who asked for the full set)
B5a. One unified type construct. The owner asked for "one universal, powerful type construct that greatly improves readability". Type theory needs only four constructors for data: product, sum, subset and constraint. Renyi spells them with one keyword and four clause words:
type User
has name: Text # product
has age: Integer where age is at least 0 # subset (refinement)
can Compare by age # ability (constraint)
end
type Shape is one of # sum
Circle(radius: Decimal)
Rectangle(width: Decimal, height: Decimal)
end
type Email is Text where value matches email_pattern # refined alias
The full design is in 02-syntax-sketch.md. (derived)
B5b. Type annotations. Every top-level function carries a full signature
(parameters, return type, failure type, effects). Inside function bodies
everything is inferred; let name: Type be ... is available when the author
wants to pin a type or guide generic inference. (user)
B6. Escape hatches. None. No any, no unsafe cast, no dynamic typing mode.
Unsafety lives only on the far side of the FFI boundary. (user)
B7. Null. Forbidden. An optional value has type maybe T; the absent value
is nothing; reading it requires otherwise or a match. (user)
B8. Object orientation. No classes, no inheritance. Data is records and
algebraic types; polymorphism is abilities. Dot syntax is kept for field access
(user.name) and for calling ability functions (shape.describe()). (user)
B9. Numbers. Integer is arbitrary precision. Decimal is exact decimal
arithmetic and is the type of literals like 19.99. Float is IEEE double and
must be chosen explicitly. There are no implicit numeric conversions. (user)
C. Syntax
C1. Overall style: clause grammar. The owner asked for a merge of the "sentence" style and the "keyword" style, with a principled basis. The basis: syntax is a map from the abstract syntax tree to strings, and three properties make it good. It is injective and invertible (one spelling per program; the formatter is the inverse map). It has low conditional entropy (given the prefix, few continuations are legal, which is what makes an LLM's output reliable). It is homomorphic to the semantics (types are products, sums, subsets; error propagation is a monadic bind; collection queries are set comprehensions and monoid folds), so surface words map one-to-one onto those structures.
The resulting design is the clause grammar: every construct is a head
followed by labelled clauses. Clauses behave like record fields: optional,
with a canonical order the formatter enforces. Mathematical notation that is
universal stays (f(x), x: Type, + - * /); everything else is an English
word. A short signature sits on one line; a long one puts each clause on its
own line. Both are the same token sequence. (user accepted the merged design)
C1a. Three verbs replace =. let x be 5 defines, set x to 6 mutates,
x is 5 tests. The language has no = symbol. This removes the =/==
confusion class of bugs entirely; an LLM that writes = by reflex gets a
compile error with the fix. (user)
C2. Blocks. Explicit end. Indentation is never semantic. LLMs are not
robust to whitespace, RAG chunking and copy-paste destroy indentation, and
end makes every block self-delimiting. The formatter sets indentation to two
spaces. (user)
C3a. Equality. is and is not. Renyi has no object identity, so there is
no Python-style identity/equality confusion. (user)
C3b. Ordering comparisons: six phrases, no symbols. The owner suggested
deriving comparison words from is. One primitive, compare(a, b) returning
Less, Equal or Greater, is provided by the Compare ability; the six
phrases are the only spellings of relations:
| Spelling | Meaning |
|---|---|
a is b |
equal |
a is not b |
not equal |
a is less than b |
compare is Less |
a is at most b |
compare is not Greater |
a is greater than b |
compare is Greater |
a is at least b |
compare is not Less |
The phrase set is closed: larger, bigger, more than are not accepted.
Rule for symbols versus words: symbols are kept for operations that nest and
grow long (arithmetic), words are used for relations, which produce a Boolean
and do not nest. (user)
C3c. Remaining symbol table. Accepted as proposed: and, or, not;
+ - * / kept; remainder and power as infix words replacing % and **;
no pipeline operator (use let steps); # comments; "Hello {name}"
interpolation; List of T generics; maybe T optionals. (user)
C4. One spelling per concept. No synonyms. There is otherwise and no
else; function and no fn or def. Common misspellings from other
languages produce a compile error with the canonical form as the suggested
fix. (user)
C4a. Multi-word keywords are single lexical units. The owner asked whether
at least is one keyword or two, and whether to write at_least. Decision:
phrases such as is at least, or fails with, for each are single tokens
recognized by the lexer through a fixed phrase table with longest match (the
same technique SQL uses for ORDER BY and IS NOT NULL). No underscores: LLM
tokenizers split on whitespace anyway, embedding models align at least with
natural-language queries while at_least reads as an identifier, and the
source reads as English. Rules: the phrase table is fixed and printed in full
in the cheat sheet; only single spaces are allowed inside a phrase and no line
breaks; every word that appears in any code phrase is a reserved word; content
words used as keywords (count, sum, first, type, end) are reserved
too and the compiler suggests a descriptive rename, which is itself good for
retrieval; the one exception is a member name after a dot (event.type,
list.count), which may be any word because position disambiguates. Words
inside free-text documentation clauses are not reserved. (user)
C5. Enforcement. Naming rules (snake_case values and functions, PascalCase
types, abilities and variants) are compile errors with a rename suggestion.
Layout (one statement per line, no semicolons, line width, indentation) is
owned by renyi format, which has no configuration. (user)
C6. Identifiers. ASCII only: a-z, 0-9, _ for values, A-Z a-z 0-9
for types. Strings and comments accept any Unicode. (user)
C8a. Documentation is part of the grammar. Every public function, public
type and every module must carry a one-sentence purpose: clause; a missing
purpose is a compile error. Private helpers may omit it. (user)
C8b. Documentation clauses. purpose:, example: (an expression and its
expected value, executed by renyi test), tags: (free words, exported to the
index), see also: (references the compiler checks for existence),
deprecated: (see C8c). (user)
C8c. Deprecation is three-tiered. The owner asked whether deprecated
functions could be "skipped entirely". Skipping compilation would break every
existing caller at once, which defeats the purpose of a grace period, so the
decision is to skip discovery instead. A definition with a deprecated:
clause is (1) removed from renyi index, LSP completion, tool export and the
main documentation listing, so agents never pick it up for new code; (2) still
compiled for existing callers, with a warning that renyi check --strict
turns into an error, and an outright "delete me" hint once it has zero
callers; (3) automatically migratable: deprecated: since 2.0, replaced by
new_name lets renyi migrate rewrite call sites mechanically when the
replacement's signature is compatible. Removing a public definition bumps the
major version automatically (see G1). (derived, accepted)
C9. Anonymous functions. None. Every function has a name and, if public, a
purpose. The query form for each ... where ... collect covers map and filter;
sorted by field, group by field, sum, count, first, any, all
cover the remaining common higher-order uses; callbacks are passed as named
functions. (user)
D. Constraints that serve LLM readers and writers
D1. Cheat-sheet token budget. The complete syntax cheat sheet must fit in 3000 tokens. It is measured in CI after every grammar change; exceeding the budget blocks the change until a feature is removed or compressed. (user)
D2. Cold start. The spec and cheat sheet travel in the prompt, the grammar skeleton stays close enough to mainstream languages for transfer, and the compiler's fix suggestions close the loop. A fine-tuning corpus is a later option, not a dependency. (user)
D3. Error output. Every diagnostic is emitted both as human-readable text
and as JSON (renyi check --json), with location, cause, and a suggested fix.
(user)
D4. Banned in v1. Operator overloading, macros and metaprogramming, implicit type conversions, wildcard imports, variable shadowing, single-letter identifiers, nesting deeper than four levels, and function bodies longer than about 60 lines. Each is a compile error. The depth and length limits exist so that every definition fits in one RAG chunk and one screen. (user)
D5. Operational definition of "retrievable code", plus content addressing.
Every definition carries a natural-language purpose; there are no wildcard
imports and no implicit context, so a chunk is understandable in isolation;
signatures and effect declarations are complete; there is no overloading and
no macro expansion, so the text is the semantics; renyi index emits one
record per definition with a stable identifier, signature, purpose, tags and
effects. In addition the owner chose content addressing: every definition
is identified by the hash of its canonical AST with dependency names replaced
by their hashes (the Unison model), so renames do not change identity, the
index deduplicates across copies, and the lockfile pins dependencies by hash.
v1 computes hashes in the toolchain; language semantics do not depend on them.
(user)
D6. Agent tools are first-class. An expose as tool clause on a function
publishes it as an agent-callable tool. Parameter types produce the JSON
Schema, purpose: produces the description, needs produces the permission
list. renyi tools lists them and renyi serve --mcp serves them. (user)
E. Runtime
E1. Concurrency. Structured concurrency on green threads. run concurrently
... end runs the statements inside as parallel tasks and waits for all of
them; the first failure cancels the rest and propagates. for each ... in ...
concurrently collect ... is the parallel query. There is no async/await,
no bare thread, and shared mutable state is impossible by construction. (user)
E2. Memory. Tracing garbage collector. Ownership and lifetimes never appear in the surface language or in error messages. The VM may mutate in place when a value is uniquely referenced; this is invisible to users. (user)
E3. Sandbox. The runtime enforces declared capabilities. renyi run grants
exactly what the program's main declares and nothing more; --deny network
style flags narrow the grant further. (user)
F. Interoperability
F0. FFI posture. First-class with boundary validation. Bindings are generated from C headers, Python type hints and TypeScript declarations, and read like Renyi functions. Return values are validated against the declared types when they cross the boundary; a mismatch fails through the ordinary error channel. (user, see T3)
F1. Compatibility scope for v1. Consuming JSON, HTTP, OpenAPI and databases (types generated from OpenAPI and JSON Schema); semantics close enough to Python and TypeScript that existing code translates mechanically; embedding Renyi as a scripting engine. Calling foreign libraries through FFI follows the posture in F0 but is scheduled after the v1 core (see H3). Transpiling Renyi to other languages is not planned. (user)
F2. "API friendly" priority. Calling external APIs first: HTTP and JSON are first-class, types are generated from OpenAPI. The language's own API (compiler as a library, AST as JSON, LSP) follows immediately at M5. (user)
F3. Host ecosystems. Python, Rust, JavaScript/TypeScript and C, all four. Implementation order: C ABI first because the other three build on it. (user)
G. Modules, packages, standard library
G1. Versioning. Built-in package manager with a central registry and a lockfile. Semantic versions are decided by the compiler, Elm-style: removing or changing a public signature forces a major bump, adding one forces a minor bump. Combined with content addressing, a version number cannot lie. (user)
G2. Standard library. Small core (base types, collections, text, time) plus official extension packages (HTTP, JSON, filesystem, regex, CSV, SQLite) maintained in the same repository and released in lockstep. Imported on demand. (user)
G3. Files and modules. One file is one module and the module name equals
the path. The module header must match the path. .renyi and .ry are
fully equivalent. (user)
H. Implementation
H1. Implementation language: Rust, one binary. The owner asked about Haskell for the compiler and why the runtime needs a language choice at all.
Why the runtime needs a systems language: the runtime is the bytecode interpreter, the garbage collector, the green-thread scheduler, the capability sandbox, the native parts of the standard library, the FFI bridge and the embedding C API. It runs underneath Renyi programs and cannot be written in Renyi, which by design has no manual memory management and no escape hatch. The platform decisions (30 ms startup, embedding, WASI and browser WASM as first-class targets) require a language without its own GC runtime, with a C ABI and mature WASM support: Rust, C or Zig. Rust is the only one with algebraic data types and pattern matching.
Why not Haskell for the compiler: it is entirely feasible (Elm and PureScript prove it) but costs two toolchains in one product, makes "compiler as a library" and embedding require linking the GHC runtime into Python, JS or Rust hosts, has an experimental WASM backend and painful Windows cross-compilation, and is not installed in the development sandbox while Rust 1.97 is. The parts of Haskell that matter (algebraic types, pattern matching, type classes, Option/Result) exist in Rust; the parts that do not (GC-backed AST sharing, laziness) are replaced by arenas and indices at roughly 1.3 to 1.5 times the code. Gleam, Roc and rustc are written this way. Haskell's heritage goes into Renyi's type system, not its implementation language. Self-hosting the compiler in Renyi is the long-term path. (user confirmed Rust)
H1b. Who writes it. Primarily Claude, in sessions. Work is sliced into units a session can finish; every unit ships with tests and a design note. The owner makes design decisions and reviews. (user)
H2. Order of work. Example corpus and cheat sheet first, then an LLM readability test (several models predict outputs and complete code from the cheat sheet; correctness rates are recorded), then the parser. The grammar is frozen by the readability test, not by the implementation. (user)
H3. First demonstrable milestone: M3. Parser, type checker and bytecode VM running the full example corpus. Milestones: M0 design documents and corpus; M1 lexer, parser, formatter; M2 type and effect checker with diagnostics; M3 bytecode VM; M4 standard library and package manager; M5 LSP, index, compiler API; M6 AOT and WASM targets; FFI generators follow M4. (user)
H4. License. Apache-2.0. (user)
H5. Documentation language. All English, including design discussion in this directory. (user)
H6. Testing from M1 on. A specification conformance suite (.ry inputs with
expected outputs, implementation-independent, reused for self-hosting); golden
tests for parse trees, formatter output and diagnostics; property tests
(formatter idempotence, parse/print round trip, type-checked programs never
crash the VM); and an LLM readability regression that reruns the model
prediction test and the cheat-sheet token count as a CI gate. (user)
I. Naming and clarifications
I1. The name. "Renyi" is the owner's online handle. Spelling is fixed as
Renyi, no accent. The collision with "Rényi entropy" in search is handled by
documentation phrasing ("Renyi programming language", "Renyi lang") and does
not constrain the design. (user)
I2. "Keyword handling" means all four. Reserved words versus identifiers
(C4a), lexing of multi-word keywords (C4a), a cap on the total number of
keywords tied to the 3000-token cheat-sheet budget (D1), and searchable
keywords in code (C8b tags: and purpose:). (user)
I3. "Format optimization" means two things. Layout rules chosen for LLM tokenization efficiency and attention locality, not only for human aesthetics; and agent-friendly formats for everything the toolchain emits (diagnostics, index, documentation). (user)
I4. Influences. Adopted: Elm (diagnostics, compiler-enforced versions, no runtime exceptions), Gleam (small static type system without type classes' complexity), Roc and Koka (effects in types, platform/application split), Unison (content addressing, documentation as a first-class citizen). Explicitly avoided: Haskell's obscure surface (custom operators, point-free style, transformer stacks, extension maze) and Python's dynamism and significant indentation. (user)
J. Round 2 decisions (session 2)
Round 2 settled the open items R2-1 to R2-17 of the syntax sketch. Each entry names the item it closes. The sketch was updated in the same commit.
J1. Named types are upward-only subtypes (R2-11). type X is Base makes
X a subtype of Base: an X is accepted wherever a Base is expected and
keeps its methods; a Base becomes an X only through construction, which is
fallible when X is refined. This is the one subtyping relation in the type
system. Rejected: fully distinct newtypes (every arithmetic step unwraps and
rewraps), plain aliases (no safety). (user)
J2. Map literals use JSON braces (R2-1). {"a": 1, "b": 2} and {}. The
notation is universal and matches the data the programs consume; a word form
would be long and error-prone. (user)
J3. Documentation clause words are reserved (R2-2). purpose, tags,
example, see, also, deprecated stay in the reserved list. One word,
one role; the compiler suggests renames (tags to labels). (user)
J4. No contracts on parameters (R2-13). Constraints live on named types
(type Attempts is Integer where value is at least 1) and are checked at
construction. Parameter signatures stay plain and call sites do not sprout
otherwise. (user)
J5. Interpolation everywhere, plus raw (R2-14). Every "..." and
"""...""" literal interpolates {expression}; a literal brace is \{; a
hole may not contain a string literal. raw "..." is a literal with no holes
and no escapes, for regular expressions and for JSON or SQL samples. One new
reserved word. Rejected: \{ alone (regular expressions become unreadable),
opt-in interpolation (the common case gets longer and the prefix gets
forgotten). (user)
J6. if and match are expressions (R2-12). Allowed where a value is
expected when every branch is a single expression or a way out (fail,
fail with, return, crash with); an if expression needs otherwise.
Six of the thirty corpus programs use the form where other languages use a
ternary operator. (user)
J7. No list patterns in v1 (R2-3). is_empty(), first() and rest()
cover the corpus. Revisit after the readability test if recursion over lists
turns out to be common. (user)
J8. Bare variant patterns; unused bindings are errors (R2-16).
when Circle then matches a variant without binding its fields. Any binding
that is never read, whether from let, a loop header, a parameter or a
pattern, is a compile error with the fix "remove it". Patterns therefore name
exactly what the branch uses, and no wildcard is needed. (user)
J9. / needs Decimal or Float operands (R2-4). Dividing two Integers is a
compile error that points to a.quotient(b) (integer division) or
a.to_decimal() / b. No operation changes the type of its operands. The
corpus never divided two Integers, so the rule costs nothing. Supersedes the
sketch's earlier "Integer / Integer yields Decimal". (user)
J10. No default method bodies in v1 (R2-5). Every implementation block spells out every method, so a retrieved implementation is complete on its own. Revisit if the corpus shows repeated identical implementations. (user)
J11. Scoped capabilities are in v1 (R2-6). A capability may carry one
literal argument naming its scope: a path prefix for filesystem and its
children, a host for network and its children, a variable name for
environment, a program name for process. A declaration without an
argument covers every scope. The checker requires each caller to cover its
callees: the same or an ancestor capability, with no argument or with one
that contains the callee's. At run time a primitive compares the actual path
or host with the calling function's declared scope and reports a mismatch
through its ordinary error type (PermissionDenied(path),
HostNotAllowed(host)). renyi run can narrow scopes further from the
command line. Claude recommended deferring this to keep the checker simple;
the owner chose precision in the source. (user)
J12. Deadlines on concurrency with within (R2-7). run concurrently
within duration and for each ... concurrently within duration ... cancel
the remaining tasks when the duration expires and fail with the built-in
TimedOut, which the enclosing function lists in or fails with. Durations
come from std.time (time.seconds(5)). One new reserved word. (user)
J13. No positional exception for commutative functions (R2-8). Two or
more arguments are always named. The standard library expresses the common
cases as single-argument methods: width.at_least(minimum),
width.at_most(limit), items.largest(), items.smallest(). (user)
J14. External field names with as (R2-9). has kind: Text as "type"
gives a field the name that ToJson, FromJson and FromRow use; it is the
only way to map keys that are reserved words or contain punctuation. A
convention-wide mapping is a decoder option:
json.parse(text: text, naming: CamelCase). as is already reserved. (user)
J15. Unused results are errors; ignore discards (R2-15). A call whose
non-empty result is not used is a compile error whose message proposes the
likely fix (set items to items.append(item)). ignore expression discards
a result on purpose. One new reserved word. (user)
J16. Decimal is IEEE 754 decimal128 (R2-17). 34 significant digits;
literals, sums and products of everyday values are exact; division rounds
half-even at the 34th digit; overflow crashes like any arithmetic domain
error. Float is IEEE 754 binary64. Rejected: exact rationals (unbounded
denominators, and printing needs a rounding rule anyway), fixed scale
(truncates rates and scientific values). (user)
J17. Single-file module names (R2-10). A .ry file run outside a project
is the module named after its file stem, and its header must say so. The
rule "module name equals path" holds with the file's directory as the project
root. (derived)
J18. Reserved words after round 2: 84. ignore, raw and within join
the 81 words of C4a. The phrase table is unchanged. (derived)
K. Standard library round (session 2)
Asked while writing 04-stdlib-sketch.md; answers shape every module.
K1. Methods are functions with self: Type (S1). A function whose first
parameter is self: T is a method of T, called value.name(...), and must
be declared in the module that defines T. Base-type methods (text.trim())
and standard-library methods (connection.query(...)) follow this one rule;
abilities exist for polymorphism, not for attaching methods. Rejected: one
single-implementation ability per type (ceremony, longer retrieval results),
module functions only (no chaining). (user)
K2. Non-2xx HTTP statuses are failures (S2). http.get(url) otherwise fail
leaves a 2xx response on the success path; other statuses fail with
HttpError.Status(url, status, body). The classic forgotten status check
cannot happen; a program that expects a 404 matches the failure. (user)
K3. matches in the prelude, the rest in std.regex (S3).
text.matches(pattern) needs no import, so refined text types stay short;
capture groups, find-all, replace and split live in std.regex on a Pattern
type. The engine is in the runtime; literal patterns are checked at compile
time. (user)
K4. Minimal Bytes in v1 (S4). Bytes is a prelude type with length,
is_empty, to_text (fallible UTF-8 decode) and to_base64;
text.to_bytes(), filesystem.read_bytes, filesystem.write_bytes, and
Response.bytes use it. (user)
K5. json.parse_with clarifies J14. There is no overloading and there are
no default arguments, so the convention-mapping decoder is a separate
function: json.parse_with(text:, naming:), json.render_with(value:,
naming:). (derived)
K6. Record patterns. A record type is matched like a single variant:
when InvalidDate(input) then. Error types such as InvalidDate and GaveUp
are records, so match and example: ... fails with ... need this. (derived)
K7. Self in ability declarations. Inside an ability declaration Self
names the implementing type, for binary methods such as compare(self, other:
Self). It is a predefined type name like Integer, not a reserved word.
(derived)
K8. Pairs. Pair of Left, Right is a prelude record with fields left and
right. A two-variable loop or query header destructures a pair;
map.entries() and items.with_index() return lists of pairs, and iterating
a map yields its entries. (derived)
K9. Maps and sets keep insertion order (R3-1). Iteration, keys(),
values() and entries() follow insertion order, so output is deterministic
and examples can compare whole maps. Python and JavaScript behave the same
way, which is what generated code assumes. (user)
K10. Variants encode as a flat object with a kind key (R3-2).
Circle(radius: 2.5) is {"kind": "Circle", "radius": 2.5} and Point is
{"kind": "Point"}; a variant with a field named kind is a compile error
when the type derives ToJson or FromJson. This is the discriminated-union
shape most APIs use. Supersedes the externally tagged draft in the first
version of the library sketch. (user)
K11. console.print takes only Text (R3-3). Other values are printed
through interpolation, console.print("{total}"). No generic entry point
converts to text implicitly. (user)
K12. Float renders with a decimal point. Float.to_text() is the shortest
text that reads back as the same value and always contains a decimal point
(5.0), so a Float is never mistaken for an Integer in output. Decimal
keeps the digits of its exponent instead (6.00 after rounded(2)).
(derived)
L. Readability test round (session 2)
L1. Models and credentials. The first live round uses Anthropic and OpenAI
models, at least three in total including the smallest model the project
intends to support (claude-haiku-4-5). Credentials are environment variables
of the cloud environment (ANTHROPIC_API_KEY, OPENAI_API_KEY), never part of
the repository or the conversation. (user)
L2. Who judges Complete and Write before M3. Claude judges each lint-clean
sample in the session and records the verdict with a one-sentence reason in
judgement.json; the owner spot-checks. The judge shares its origin with some
of the tested models, so every verdict is re-run by the VM at M3 and the two
sets of results are compared. (user)
M. Pre-test round decisions (session 4)
The readability pre-test (tests/readability/2026-10-05-e41258c/notes.md)
raised these questions; the owner answered them in one batch.
M1. group by combines with every terminal clause. for each sale in
sales group by sale.region sum sale.amount is a Map of Text to Decimal:
the terminal after group by is applied to each group, so collect, sum,
count, first, any and all all work per group, and a bare group by
keeps whole items as before. Both models of the pre-test wrote this form for
the same program and the parser already accepted it. (user)
M2. The loop variable of a bare count query is unused. In for each
line in order.lines count the variable line is never read, so decision J8
applies and the fix is order.lines.length(); one concept keeps one spelling.
The variable of a first query counts as read because first returns it.
(user)
M3. otherwise on a value that cannot fail is an error. otherwise is
allowed only after a maybe value or a fallible call. Port(8080) otherwise
crash with ... (a literal checked at compile time) and line.split(" ")
otherwise fail are compile errors whose fix is "remove otherwise: this
cannot fail". (user)
M4. A named single argument is an error. column_widths(table: table)
is rejected with the fix "drop the name": one argument is positional, two or
more are named, and the formatter never changes tokens. The same holds for a
construction with one field (UserId(id: 7)). (user)
M5. The readability scoring formats before it lints. renyi format runs
on every Complete and Write program before the lint, since an agent runs the
formatter anyway; layout violations are still counted and reported per rule
but no longer fail a sample. 03-readability-test.md is updated. (user)
M6. The cheat sheet gains a library section. Names only (prelude text, list, map and set methods; the standard modules), within the 3000-token budget. Invented library names were the largest cause of Sonnet failures. (user)
M7. Haiku 4.5 stays the floor model (L1). Its pre-test rates (Complete 37%, Write 0%) are from one sample per item; the cheat sheet is iterated on the failure causes first, and the live round with five samples decides. (user)
M8. An MCP server for the toolchain, after M2. A server that offers the
cheat sheet, a library-name lookup, check and format (later run and
test) to any agent host is scheduled after the type checker, when check
has substance; whether it reuses the renyi serve --mcp command of decision
D6 or gets its own subcommand, and whether a JSON dependency is taken, is
decided then. (user)
M9. repeat until replaces while. The only condition loop is
repeat until condition ... end: the condition is tested before each pass
and names the state that holds when the loop is over, so the reader gets the
postcondition without negating anything (repeat until remaining.is_empty()
instead of while not remaining.is_empty()). repeat until is one phrase
token; repeat and until are reserved and while leaves the list, which
has 85 words (supersedes the count in J18). while becomes a foreign
keyword: the compiler rejects it and suggests repeat until with the
opposite condition. The owner asked for a simpler, fixed loop shape;
for each is unchanged. (user)
M10. No bottom-tested loop. The "do, then test" case (pagination) keeps
its flag variable or uses break; the corpus has one such loop. Revisit after
the live readability round. (user)
N. Decisions forced by the type checker (session 4, M2)
Derived while turning the library sketch into declaration files the compiler
reads (library/std/*.ry) and writing the checker; each follows from an
earlier decision and is recorded so that it can be questioned.
N1. A method may be named with a reserved word. The library declares
first, at, set, sum, sorted and repeat as methods, and the
corpus calls them after a dot, where any word is allowed (C4a). A function
whose first parameter is self is only ever called after a dot, so the
parser accepts any word as its name; an ordinary function keeps the rule.
Parameters are bound as plain names and keep the rule too, so the library's
count parameters became length, days and amount. (derived)
N2. Path, Url and Pattern are plain subtypes of Text. Their
validity is checked by the operations that use them (a missing or malformed
path fails with FileError, a malformed URL with Unreachable) and, for
literals, at compile time by the toolchain. Constructing them from runtime
text therefore needs no otherwise, which is how the corpus already writes
Url("{base}?{query}") and Path(path_text); the sketch's where value is
not "" on Path is dropped. (derived)
N3. Library error types derive ToText. The corpus logs errors with
error.to_text() in failure arms, and the cheat sheet shows the same, so
every error type the library declares carries can ToText; a program's own
error type adds the clause when it is rendered. ToText stays opt-in for
other types. (derived)
O. Runtime and agent tooling (session 4)
Asked by the owner as three questions (how memory is managed, what sets the
language apart, what the language can offer LLM agents); the owner took the
recommendations as given. The design is in 05-agent-tooling.md.
O1. Memory is reference counting with in-place reuse; supersedes E2.
Renyi's values are immutable, there are no references, no closures (a
function is passed by name and captures nothing), and no shared mutable
state, so the value graph is acyclic: a value cannot contain itself.
Acyclic data needs no tracing collector. The VM counts references, frees a
value when its count reaches zero (so a file or a connection closes as soon
as it is dropped, deterministically), and updates a value in place when its
count is one, which makes set items to items.append(item) an O(1) append
when nothing else holds the list (the Perceus/Koka and Roc model; the
compiler marks last uses so that arguments are moved, not copied). The v1
scheduler runs green threads on one OS thread, so counts are not atomic;
when tasks move to several threads, values that cross a task boundary get
atomic counts. The user-facing promise of E2 stands unchanged: ownership
and lifetimes never appear in the language or in a diagnostic. A future
lazy must keep the graph acyclic (a thunk may not refer to its own
binding). (user, on Claude's recommendation)
O2. The project map is the agent's view of a project. renyi index
(decision D5) emits one record per definition and one per module, as JSON:
identity (content hash), kind, signature, purpose, tags, declared and
transitive effects, declared and transitive failure types, the edges
(calls, type uses, implementations, tests), six metrics (lines, nesting
depth, branch points, effects, fan-in and fan-out, example and test
coverage) and the source location. Every number is exact because the
language has no macros, no overloading and no dynamic dispatch outside
abilities. The map is incremental by hash and is what an agent reads
instead of files; granularity is module to definition, with tests as nodes
of their own. (user)
O3. Complexity budgets. The per-definition limits of D4 (nesting depth 4, about 60 lines) stay compile errors. Module-level budgets (public definitions per module, size of the transitive effect set, fan-out) are reported by the map and gated in CI as warnings first; thresholds are set once the map has been measured on the corpus (open item R5-1). (user)
O4. Effect and semantic diffs. renyi index --diff compares two maps
and reports changed signatures, effects and failure types with the callers
each change reaches; the version-bump rule of G1 reads the same diff, and
an agent checks that its change did not widen any grant. Scheduled after
M3. (user)
O5. The toolchain MCP server is renyi mcp. Decision M8's server gets
its own subcommand, distinct from renyi serve --mcp (D6), which serves a
program's own expose as tool functions. renyi mcp speaks JSON-RPC over
stdio and offers project_map, definition, effects, check,
format, cheat_sheet, library_lookup, and after M3 run, run_tests
and diff. The checker's World stays resident and is refreshed by
content hash. serde_json is taken as a dependency: the zero-dependency
rule ended when regex entered the checker, and a hand-written parser
would buy nothing. (derived)
P. Signature capabilities (session 4)
The owner asked for at least one capability no other language has had, new,
practical and elegant, and chose three of the four candidates. Each rests on
the effect system: every effect goes through a declared capability and a
small set of library primitives, so the runtime sees all of them. Design:
06-runtime-guarantees.md.
P1. Recorded runs. renyi record program.ry runs a program under its
grant and writes every effect call (capability, primitive, arguments, result
or failure, clock and random values) to a recording. A test declared with
replays "path" answers its effects from the recording, deterministically
and offline; a call the recording lacks fails the test. renyi run --replay
re-executes a recorded run for debugging, and --explain narrates any run
with the purpose: clauses of the definitions it passes through, the data
they produced and the effects they used (the self-narrating layer). The
effect system guarantees the recording is complete; the ToJson rules give
it a readable format that the replay validates against the declared types.
One new reserved word, replays. Nearest prior art: VCR-style HTTP
recording libraries and Darklang's trace-driven development, neither a
language feature with a completeness guarantee. Scheduled in M3. (user)
P2. Budgets in grants. A capability in the grant of main or of a
test may carry at most COUNT per UNIT (units second, minute, hour,
day, run); the runtime counts primitive calls under it and fails the
call that exceeds the budget with the module's error type. Budgets apply to
network, process and filesystem. One new reserved word, per, and
the phrase at most. Scheduled in M3. (user)
P3. Provenance guards. A capability in the grant may carry only to
SINK or SINK: values that enter through it, and values computed from them,
carry that origin and may leave the program only through the listed sinks;
any other outgoing primitive fails with Guarded(origin, sink). This is the
language-level answer to exfiltration through prompt injection, the central
risk of agent sandboxes. The runtime tracks origins as a small set per heap
value, which Renyi's immutable values and explicit effects make cheap.
One new reserved word, only, and the phrase only to. The syntax is in
the grammar now so that the cheat sheet, the parser and the live
readability round see it; the runtime part is designed first and scheduled
in M4. Prior art: information-flow languages (Jif, FlowCaml, LIO) and
runtime taint modes (Perl, Ruby); none ties the policy to the capability
grant. (user)
P4. Reserved words after P1 to P3: 88, phrases 17. only, per,
replays join the list (supersedes the count in M9); at most and only
to join the phrase table. Budget units are plain words after per, not
reserved. (derived)
Q. System-level commitments (session 4)
The owner asked for system-level innovations that solve pain points of
mainstream languages outright and resolve their "cannot have both"
trade-offs; all four candidates were taken. Design: 07-system-design.md,
whose section 1 states the trade-offs the language claims to resolve.
Q1. Capability-safe packages. A package's effect manifest is computed
by the toolchain at publish and recomputed by the registry; main's grant
must cover the transitive effects of every dependency the program reaches,
with the package and capability named in the error; an update whose
effects widen is refused unless the grant covers them and the user accepts;
scopes narrow dynamically across package boundaries through the grant
stack (the effective grant is the intersection along the call chain, which
every primitive checks); native code is visible as foreign. Supersedes
the wording of J11 for cross-package calls; within a program the static
coverage rule stands. M3 (grant stack), M4 (packages). (user)
Q2. Reproducibility by construction. A run manifest names everything a
run depends on (toolchain, code and dependency hashes, grant, arguments,
environment variables read, recording); renyi reproduce replays it and
compares byte for byte; builds are functions of the same hashes. Rests on
P1, K9 and D5. M3 and M4. (user)
Q3. In-process sandboxing. The effect grant is the isolation boundary: a host loads a module with a grant (capabilities, scopes, budgets, guards, a memory budget) and the module can reach nothing else, with no process or container; several modules with different grants share one process through the grant stack on the task tree. After Q1; embedding API at M5. (user)
Q4. Checked live update. A running service swaps the changed definitions of a type-checked new version between requests, by content hash, after the semantic diff shows no public signature a running definition calls has changed; there is no state to migrate because the language has no hidden mutable state. M5. (user)
R. Decisions from the first live readability round (session 5)
The round (tests/readability/2026-10-05-1623155/, its notes.md) ran
Sonnet 5.5, Haiku 4.5 and gpt-5.4-mini with five samples per prompt. By the
four-of-five rule: Sonnet Predict 90, Explain 97, Complete 79, Write 40;
Haiku 30, 90, 37, 0; gpt 80, 97, 32, 0. Each measured cost below was put to
the owner as a question with the recommendation first.
R1. The freeze is gated on the large models. The acceptance thresholds
of 03-readability-test.md apply to the current large model of each vendor
(Sonnet 5.5 and its successors, gpt-5.5 from the next round); the floor
model of M7 (Haiku 4.5) is run and reported as a trend and does not gate
the freeze. Haiku failed Predict at 30 percent through arithmetic and
attention errors (mis-added means, dropped parentheses, wrong column
widths), none of them a grammar misreading, so "every model" would never
freeze the grammar. Supersedes the wording "on every model" of the
protocol; M7 stands. (user)
R2. J3 stands: reserved words are reserved everywhere. The query words
(count, sorted, first, sum, ...) stay reserved outside queries too.
Measured cost: Sonnet loses two Complete items to count as a local name
(79 percent against 89 without the rule); Haiku and gpt lose more to
sorted and first. One word, one role, is worth it. (user)
R3. purpose: stays required on public types and on the module. It is
the largest single Write rule (28 percent of Sonnet's violations, over the
protocol's quarter; 25 percent of gpt's). The rule is kept because the
project map (O2) reads those purposes; the cheat sheet states it in one
explicit sentence and every public type in its examples carries one, since
the readers copy the examples. (user)
R4. M4 stands: a named single argument is an error. It is the largest checker rule for Haiku (92 samples) and gpt (77). The uniformity (one argument positional, two or more named) is kept over the pass rate. (user)
R5. is on numbers. Both operands must have one type: an Integer and
a Decimal do not compare (no implicit conversion, as the cheat sheet
says). A numeric literal takes the numeric type its context expects (an
Integer literal fits Decimal and Float, a Decimal literal fits Float),
so total is 0 with total: Decimal compares two Decimals. Two Decimals
compare by numeric value; the scale is not significant: 32.0 is 32.00 is
true, and so is 0.00 is 0 through the literal rule. This is what the
judges of the round, the checker's refinement evaluation and IEEE
decimal128 comparison already do. (user; the value reading derived from
J16)
R6. ignore discards only the result of a call with effects. Supersedes
the wording of J15: ignore call is allowed when the call needs a
capability (the callee's needs, or those of a function passed to it);
discarding the result of a pure call is a compile error, ignore-pure,
whose fix proposes set x to x.method(...) for a method on a mutable
binding and otherwise "bind the result with let, or remove the call".
Three judged samples of the round wrote ignore pop(stack) on an immutable
value and so never shrank the stack; under this rule the checker catches
it. The error for a call returning nothing (ignore-nothing) stays. (user)
R7. Budget thresholds (R5-1). The first defaults of renyi index
--budgets are 10 public definitions per module, 5 transitive effect paths
per module and fan-out 7 per definition, each just above the corpus
maximum; a value over budget is reported, never a compile error (O3). The
thresholds move to the project manifest with the package manager (M4).
(user)
R8. A fourth model from the next round. gpt-5.5 rejects temperature 0
and was not run; it joins the next round at its default temperature, as
Sonnet 5.5 ran in this one, so that each vendor has a large model gating
the freeze (R1). (user)
S. Runtime dependencies and concurrency (session 6, M3)
Each question was put to the owner with the recommendation first, after a probe crate had built the candidates on the owner's machine (GNU toolchain, 31 seconds, all three working).
S1. Three dependencies for the runtime. The HTTP client of std.http
is the ureq crate (version 3, rustls with the ring provider: no system
TLS, no C code for the client); std.sqlite is rusqlite with its
bundled feature (the SQLite sources compiled into the binary, which needs
a C compiler on the build machine); std.server is written over
std::net::TcpListener (HTTP/1.1, one request at a time, no TLS, which a
local service or one behind a proxy does not need). Redirects are followed
by the runtime one hop at a time so that every host a request reaches is
checked against the grant. Alternatives declined: a hand-written client
(no TLS, so the corpus could only run from recordings), reqwest (tokio),
tiny_http, and leaving any of the three unbuilt. (user)
S2. Tasks run one after the other. run concurrently and
concurrently queries execute their tasks in source order on one thread,
with the within deadline checked between statements and items. The
meaning of a program is unchanged: tasks are independent, so their order is
unobservable except through time, and values stay single-threaded
reference counts (O1). Overlapping I/O waits (green threads on one OS
thread) is a later runtime improvement, not a language change; a thread
per task was declined because values would have to cross threads. (user)
S3. Redaction by name. renyi record --redact NAME (also renyi test
--refresh) replaces, in the recording, the value of an argument named
NAME, of an entry named NAME (ignoring case) inside a map argument such as
a header list, and of the environment variable NAME as environment.get
returns it, with the placeholder <redacted>; a replay matches the
placeholder against any value, so a recording made with a real key replays
with any key. Redaction is by name, not by data flow: a secret that
reaches another argument is recorded as it is (provenance guards, P3, are
the data-flow mechanism). Settles open item R6-3. (user)
S4. Response bodies stay inline. A recorded HTTP response keeps both
its text (body) and its bytes (bytes, base64) in the recording, with
no size limit and no side file; the corpus fixtures are small, and a
side-file format can be added when a recording needs one. Settles open
item R6-1. (user)
S5. The run manifest lives in the recording. renyi record writes
the manifest of decision Q2 into the header of the recording it makes:
the toolchain, the source path, the content hash of main (which covers
everything main reaches), the arguments, the environment variables read
with the hash of each value or the redaction placeholder, the outcome, and
the SHA-256 and length of the standard output. renyi run --manifest
prints the same header and writes no file. renyi reproduce FILE
[program] refuses when main hashes differently, warns when the
toolchain differs, replays with the console output written, and reports a
different outcome or output and any recorded call not reached. The owner
asked whether the manifest could live in the .ry file itself; declined
because a manifest describes one run of a program rather than the program,
every record would rewrite the source, and the grammar under measurement
would grow. A separate manifest file was declined to keep one file per
run. Until the registry (M4) the code is read from the path the manifest
names and checked by hash. (user)
S6. No narration of query clauses. --explain narrates a function's
entry with its arguments, its result, and each effect; a query inside it
(for each ... where ... collect) is not narrated as a sentence of its
own, since the function's result shows what the query produced and a line
per query would lengthen the trace. Settles open item R6-2. (user)
S7. A budget is a number. at most COUNT per UNIT takes a literal
count, not a constant: the budget sits in the signature of main or of a
test so that the reviewer reads the number there. Settles open item R6-4.
(user)
T. The MCP server (session 6)
renyi mcp (decision O5) was built after M3; each question was put to the
owner with the recommendation first.
T1. Both eras of the protocol. The current revision of the Model
Context Protocol (2026-07-28) has no handshake: every request names its
version and the client's capabilities in _meta, and the server must
answer server/discover; the hosts in use today still open with the
initialize handshake of revision 2025-11-25 and earlier. renyi mcp
speaks both, as the specification's compatibility section allows a
"dual-era" server to: a request with a version in its _meta is answered
statelessly as the current revision says, and an initialize request
selects the handshake. Supporting only the handshake would leave the
server behind the specification; supporting only the current revision
would leave every host on this machine unable to connect. (user)
T2. The VM's JSON reader serves the protocol. Decision O5 took
serde_json as a dependency for the server; since then the VM has its own
JSON reader and writer (std.json, the recordings of decision S4), which
the server uses instead, so the toolchain keeps one JSON implementation
and no new dependency. Supersedes that sentence of O5; the rest of O5
stands. (user)
T3. run and run_tests take what the command line takes. Besides
the path and the arguments of section 7's table, the run tool accepts
the narrowing options of renyi run (deny, allow_host, allow_read,
allow_write, at_most), a replay recording and explain, and
run_tests accepts strict and explain, so that an agent can sandbox
or replay what it runs exactly as a person can. (user)
T4. The map is refreshed by file, not by definition. Section 7 of
05-agent-tooling.md said the server refreshes only the definitions whose
content hash changed; the server rebuilds the whole map when any file of
the served directory differs from the one the last map was built from.
The result is the same, and the corpus (30 modules) rebuilds in about a
second in a debug build; the per-definition refresh is open item R5-5,
for projects large enough to need it. (user)
T5. The diff reads a definition's own text hash beside its content
hash. Section 6 of 05-agent-tooling.md reported a body change from the
content hash; the content hash of decision D5 also changes when anything
the definition depends on changes, so a one-line change in a helper would
read as a body change in every caller, and the origin of a change would be
lost. Each record now also carries text_hash, the hash of the
definition's own canonical text with its own name and every reference to a
project definition blanked (the first two edits of D5, the references
blanked rather than replaced by hashes, and without the dependency and
library material). The diff reports a body change where text_hash or the
edges changed and lists the callers under what the change reaches; a
definition is matched across the maps by its qualified name, and a name
gone whose content hash is back under another name is one rename entry,
its callers untouched, as D5 intended. A map written before this field
falls back to the content hash. The base of a diff is a map file or a git
revision; a revision's files are read with git show and indexed afresh
with the same toolchain, so that the diff never needs a map to have been
saved. The version bump of G1 is derived from the entries: a public
definition removed, its signature changed (the needs and or fails with
clauses included), made private or renamed is a major bump; one added or
made public a minor bump. (derived)
U. Readability round 2 (session 6)
Round 2 (tests/readability/2026-10-05-ed37120/notes.md) raised four
questions; each was put to the owner with the recommendation first.
U1. M4 stands; the cheat sheet says it in a sentence. "A call with one
argument does not name it" (decision M4) was again the largest checker rule
for all four models of the round (43, 50, 90 and 68 error lines for Sonnet
5.5, gpt-5.5, Haiku 4.5 and gpt-5.4-mini) and cost gpt-5.5 the Write item
fizz_words outright. The recommendation was to let a single argument be
named or not; the owner keeps the rule, as after round 1, and the cheat
sheet states it in a sentence beside the call examples ("One argument is
never named: web.get(url), not web.get(url: url)") instead of a comment.
Measured again in the next round. (user)
U2. rounded keeps its places; the cheat sheet shows the call. Both
gating models wrote value.rounded() and decimal.to_decimal() and lost
invoice and shapes (Complete) and compound_interest (Write) to it;
gpt-5.5 would pass Complete without those two items. The recommendation
was a library change (places defaulting to 0, to_decimal an identity on
Decimal); the owner keeps the library as it is and has the cheat sheet
list the method as rounded(places: 2), so that the argument is seen
before it is needed. (user)
U3. Every method call takes parentheses; the cheat sheet lists methods
so. The models wrote text.length, items.is_empty and items.last as
fields (21, 100 and 20 error lines for gpt-5.5, Haiku and gpt-5.4-mini)
because the library section of the cheat sheet listed the method names
bare. The owner asked whether a form without parentheses should be spelled
length of items; that would be a second spelling of one call, against
the one-spelling rule of the cheat sheet's first line, and no model had
written it, so the grammar is unchanged and the cheat sheet lists every
library method with its parentheses (length(), is_empty(), ...). (user)
U4. Explain is graded by Claude Code subagents; round 1 is re-graded the
same way. Round 2 graded Explain with subagents (Sonnet 5.5 first, Opus
5.5 second, fifty samples a batch, a reason kept with every grade) because
the owner ruled during the round that the pay-per-token API keys of
decision L1 are not spent by default and subscription quota is used
instead. Measured against the API grades collected before the ruling, the
subagents are about one point more lenient and flip 30 of 204 floor-model
samples from fail to pass, none the other way. The protocol needs one
grader kind across rounds, so the subagents grade Explain from here on,
round 1 is re-graded with them so that the series is on one scale (its
API-graded tally is kept beside the new one), and the API graders are used
only when the owner authorizes the keys for a round. The harness now
snapshots the references into each run directory at prepare, after a
description went stale between the rounds. L1's credentials sentence
stands for the API path; this entry adds the default. (user)
U5. A hand verdict extends to every sample that repeats the sentence it
called wrong. The re-grade of round 1 (U4) showed the subagent graders
agreeing on a passing grade for three statements the hand verdicts call
wrong (Sonnet's retry.3, Haiku's inventory_db.2 and
shipping_rules.0), and the adjudication rule of the protocol reaches
only samples the two graders disagree on by more than one point. From
here a hand verdict of 2 on a sentence applies to every sample, of any
model in any round, that repeats the sentence or makes the same statement
about the same program; the adjudicator searches the program's samples
for it and records a verdict that names the originating one. Applied to
both rounds in session 6, fourteen verdicts: in round 1 Haiku's
inventory_db.1 to .4, shipping_rules.0, invoice.2 and .4 (where
a session 5 verdict of 4 had read "quantities and prices are
non-negative" as correct and is withdrawn, the quantity being at least
1), Sonnet's shipping_rules.1 and retry.3; in round 2 Haiku's
inventory_db.0 to .4 ("items that have fallen below their reorder
threshold" for at or below). Haiku's Explain is 27 of 30 in round 1 and
26 of 30 in round 2 after it; no gating model's item moved. The
alternatives were to keep the rule (the floor model's number then
carries grades the hand would not give) and a hand pass over every
agreed grade (1050 samples, declined as not worth it while the gating
models are unaffected). (user)
U6. The five-point rule applies to the gating models. The protocol's
rule that a grammar change lowering any passing rate by more than five
points is reverted or explained did not exempt the floor model, and
Haiku 4.5's Explain fell between the rounds on the one scale (97 to 90
before U5, 90 to 87 after it) on wrong statements about todo_cli,
invoice and inventory_db, not on any changed rule. The floor model is
reported as a trend and does not gate (R1), and its reading errors would
trigger the rule every round. The rule now names the gating models; the
floor model's moves are reported and explained in the round's notes.
03-readability-test.md amended. (user)
U7. Round 3's samples come through the subscription channels. The
default of U4 (no pay-per-token keys unless the owner says so for the
round) applies to collecting samples too: the Claude models are sampled
by Claude Code subagents and the OpenAI models by the Codex CLI on the
ChatGPT subscription, each sample fed to the harness with run --provider
file (round 2's gpt-5.5 Write samples came that way). The cost is
comparability: a subagent cannot be sampled at temperature 0 and sees the
host's system prompt around the cheat sheet, so the floor model's rates
carry more noise than in rounds 1 and 2; the gating models already ran at
their default temperature. The owner may still authorize the keys for a
round, and then the API path of run is used as in rounds 1 and 2.
03-readability-test.md amended. (user)
U8. The library list carries parameter names; a sentence beside it gives
the call form. Round 3 (tests/readability/2026-10-06-3e7c45a/notes.md,
Sonnet 5.5 through the Claude Code CLI) measured the cheat sheet after U1
to U3 and found the rendering of U2 and U3 wrong: the list showed
rounded(places: 2), a single argument named, which decision M4 forbids
in a call, and split() beside length(), which hides the separator.
Sonnet copied both as shown: rounded(places: 2) in every sample of
invoice and shapes (Complete) and compound_interest (Write), all
rejected by the checker ("a call with one argument does not name it"),
and split() in three of five word_count samples ("split takes 1
argument (separator), found 0"); its Complete fell from 89 to 68, with
shapes and word_count lost to the rendering and deadlines and
invoice_report to the reserved word count. The rendering was
Claude's reading of U2 and U3, not what they decide (the argument seen
before it is needed; every method a call): both stand, in this form. The
list now gives every method with its parameter names (split(separator),
rounded(places), replace(old, new), set(key, value)), checked
against library/std/prelude.ry, and one sentence after it gives the
call form of M4: line.split(","), price.rounded(2),
text.replace(old: "a", new: "b"). Paid for by shortening the
phrase-token list to three examples, dropping the nesting and body
limits, the count of reserved words and "(explicit)" after Float; 2998
tokens. Round 3 is a defect round: its rates measure the defect, and
round 4 on the corrected sheet through the same channel (Sonnet 5.5
first; gpt-5.5 when the Codex quota or the owner's key authorization
gives it a channel) is the measurement of U1 to U3. count stays
reserved (R2); the owner waits for gpt-5.5's data before reopening it.
(user)
U9. The cheat sheet says when a refined construction takes
otherwise. Round 4 (tests/readability/2026-10-06-c696747/notes.md,
Sonnet 5.5 on the sheet after U8, through the CLI channel run in an empty
directory with no MCP server) shows the errors U1 to U3 and U8 addressed
gone: no named single argument, rounded() or field-form error in 345
samples, against 43 named-argument lines in round 2. Complete is 79
against round 2's 89 on four items whose slips no changed sentence
touches, and two of them are one gap: Port(8080) otherwise crash with
... on a literal the checker proves valid (superfluous-otherwise,
config) and Done(position: number) without otherwise after if
number is less than 1 then return Help end (missing-otherwise,
todo_cli); the sheet said only that refined construction can fail. It
now says "A literal the checker can evaluate needs no otherwise
(Port(8080)); a variable needs one even after a check", paid for by the
phrase-token sentence, a query comment and "renyi run enforces it";
2991 tokens. The checker is
unchanged (a flow-sensitive refinement, which would read the check, was
the alternative and is declined for now). The owner also ruled that the
notes' item-by-item explanation satisfies the five-point rule for the
fall of Complete without a decision entry, since no change caused it,
and that gpt-5.5 does not run on this sheet for now: the rounds continue
on Sonnet, and the freeze still needs the second gating model (R8).
(user)
V. The gap audit and the road to self-hosting (session 6)
On 2026-10-06 the owner set the goal beyond the readability scores and
answered the questions of the gap audit (docs/GAPS.md) in two batches of
four, the recommendation first in each.
V1. The goal is a self-hosted language with users; the grammar freezes
by decision. The readability rounds did their work (the sheet and the
corpus are what the gating model reads at 90, 100, 79, 40); the aim from
here is a language that stands on its own: a front end written in Renyi
with the Rust VM as the runtime, a formal grammar and a reference, first
users among people who build agent workflows and among learners, and a
corpus that future models meet in their training data. The grammar is
frozen by a decision entry rather than by the thresholds of
03-readability-test.md, and that entry is written only after the
implementation does what the design documents say: stage 1 of
docs/GAPS.md section 7 (the defects, the promised checks, the four
parsed-but-empty features, the document corrections, CI and the
conformance suite) comes before the freeze, then the formal grammar and
the reference, then the self-hosted front end, then the milestones M4 to
M6. The repository stays private until the owner says otherwise.
Supersedes the freeze-by-measurement sentences of H2, R1, R8 and U9; the
readability harness remains a measurement the owner can ask for. (user)
V2. Indentation carries no meaning, continuation lines included. The
parser contradicted C2: a statement continued on the next line only when
that line was indented deeper than the line that started it, and a
documentation clause continued on deeper lines (sketch section 1 described
both). The owner chose to make the language indentation-insensitive
outright rather than record the exception. A statement or clause now ends
at the newline unless a bracket is open or the next non-blank line starts
with a continuation word (where, sorted by, group by, collect,
sum, count, first, any, all, returns, or fails with,
needs, for any, with, and, or, and inside an example: is
and fails with); the value after be or to may start on the next
line. otherwise is not a continuation word: at the start of a line it
opens the branch of an if or match, so the otherwise that guards a
value stays on the line of that value and the formatter breaks inside the
parentheses of the longer argument list, the call's or the fallback's,
instead (otherwise-line is the error for an otherwise that starts a
line where no branch can). return keeps its value on its line, since return
alone is a statement and the next line could start one. The free text of
purpose:, tags:, see also: and deprecated: runs to the end of its
line. The corpus was rewritten by the formatter (eleven otherwise lines
and three return lines joined); no documentation clause of the corpus
wrapped. Sketch sections 1, 13 and 16 amended. (user)
V3. Ordering is Less, Same, Greater. The library has said
Same since the declaration files were written, because Equal is the
name of the equality ability and a variant of that name would read as a
type test in when Equal then. C3b's table names the result of
compare; it is read with Same in place of Equal. Supersedes the
variant names of C3b. (user)
V4. The phrase table lives in the syntax sketch. C4a said the fixed
phrase table is printed in full in the cheat sheet; U8 and U9 shortened it
to three examples to pay for the library parameters and the
refined-construction sentence. The table's normative place is section 17
of 02-syntax-sketch.md, and the language reference once it exists; the
cheat sheet shows examples. Amends C4a. (user)
V5. The nesting and body limits are compile errors: depth 4, 60 lines.
D4 and O3 promised them and the checker never enforced them. A statement
nested deeper than four blocks (an if, match, loop or run
concurrently body adds one; the function body is depth 0) is
nesting-depth, and a function body whose statements span more than 60
source lines is body-length, each with the fix "move the inner part
into a function". The corpus's deepest body is 3 and its longest 30
lines, so nothing in it changes. (user)
V6. The four parsed-but-empty features. only to gets its runtime
now, as section 3 of 06-runtime-guarantees.md designs it (P3 had
scheduled it in M4). deprecated: gets tiers 1 and 2 of C8c now: a call
to a deprecated definition is a warning that renyi check --strict turns
into an error, and renyi index drops the definition; renyi migrate
(tier 3) waits for the package manager. expose as tool gets renyi
tools, which prints the manifest of D6 (JSON Schema from the
parameters, the description from purpose:, the permissions from
needs) and the checker rejects a parameter or return type that JSON
cannot represent; renyi serve --mcp waits for M5. process and
foreign leave the cheat sheet, and the checker rejects them in a
needs clause (capability-unavailable) until the package manager
brings std.process and the FFI (M4). Nothing in the frozen surface
parses and does nothing. (user)
V7. Task independence is a checker rule. E1 promised that a race
cannot be written, and S2 delivers it only because tasks run one after
the other. The checker now enforces independence: a task of run
concurrently may not set a mutable binding declared outside the block
and may not read a binding declared by another task of the same block;
the violation is task-independence with the fix "bind the result with
let inside the task, or run the statements in sequence". The body of a
concurrently query is one expression per item and is independent by
construction. Real concurrency (green threads) remains a runtime
improvement that the rule makes safe. (user)
V8. CI and the conformance suite. D1 and H6 named them; neither
existed. A GitHub Actions workflow runs cargo fmt --check, cargo
clippy --all-targets, cargo test, the cheat-sheet token gate and the
corpus lint on every push; the private repository's minutes are the
owner's. The conformance suite is tests/conformance/: Renyi programs
with their expected standard output, exit code and diagnostics, in files
a compiler written in Renyi can be tested against without the Rust
crates; the corpus's reference outputs are its first members. (user)
V9. A passed function runs under the grant in force where it is
called. B1 charges the needs of the function actually passed to the
call site that passes it, whatever the parameter's type lists, and a
higher-order function declares only its own effects. At run time the
grant stack of Q1 applies as everywhere else: the passed function runs
inside the frame of the function that calls it and is narrowed by that
function's needs (sketch section 3). A function value stored in a
collection and called later is charged nowhere statically and is refused
by the grant stack at run time (docs/GAPS.md, 1.11). (user)
V10. Iterable is one method, to_list. public ability Iterable of
Item declares function to_list(self) returns List of Item, without
needs: an implementation's method never declares effects
(method-signature otherwise). for each and the queries walk any type
that implements it, through the list to_list returns, after the
built-in collections; the item type is the implementation's type
argument (ability Iterable of Card for Deck), with the implementation's
for any parameters substituted. A cursor protocol (next) and
constraints with type arguments (where Bag can Iterable of Item, open
item R3-2) are not in v1. Supersedes the Iterable of the early
core-ability list, which was declared nowhere. (user)
V11. The grammar is frozen. The surface syntax is what
02-syntax-sketch.md and docs/cheatsheet.md describe at commit
dc58bb3 (2026-10-06), with the corpus under examples/ and the
conformance programs under tests/conformance/programs/ as its
witnesses: the reserved words and phrases of sketch section 17, the
clause grammar of sections 2 to 15, the canonical layout of section 16,
and the core abilities of section 5 with Iterable (V10). From here a
change to the surface is a new decision entry that names what it
supersedes; it lands in the sketch, the cheat sheet (within its budget),
the formatter and the conformance suite in one commit, and the old
spelling gets the new one as its fix (C4). The open items of sketch
section 18 (R3-2) and docs/GAPS.md section 6 are not surface changes
until decided; library additions (04-stdlib-sketch.md) are not surface
changes. The formal grammar (EBNF) and the language reference are
derived from the frozen sketch in stage 2; what they find unclear is
settled by an entry, not by the implementation. (user)
V12. Line breaks after commas and before is and fails; one spelling
per construct. Three rulings the formal grammar required, all on the
line rule of sketch section 1. (1) A line break after a comma carries no
meaning wherever the comma stands: a needs list, a for any list, an
exposing list, a can Compare by list, a with update or the sources
of a query may continue on the next line, as the example of section 11
always showed; the formatter breaks a needs clause wider than the line
one capability per line, indented once more than the clause. (2) is
and fails are continuation words everywhere, not only inside an
example:: the continuation words are one list read without context,
which the grammar's token filter and a self-hosted lexer can state; the
formatter breaks before them only in an example. (3) The parser accepts
one spelling of each construct, since the formatter never changes a
token (section 16): it rejects, with a fix, a trailing comma in a
bracketed list, a variant with empty parentheses in a declaration or a
pattern, a space between a dot and the member after it, an otherwise
arm that is not the last of a match expression, a head and end on
one line, a hole in a test name, a capability scope, a recording's path
or an external name, and public on an ability's method
(public-method); and it accepts, as section 1 says, a line break
inside brackets before a dot or a call's parenthesis. The grammar
docs/grammar.ebnf and its test crates/renyi_syntax/tests/grammar.rs
state the surface under this entry. (user)
W. The self-hosted front end (session 8)
W1. The syntax tree's JSON is the derived JSON of the Renyi AST.
compiler/ast.ry declares the syntax tree as Renyi types, one per
construct, each can ToJson; the document renyi parse --json prints is
their derived JSON: a record is an object whose keys are its fields in
declaration order, a variant is an object with kind first and its
fields after it, a maybe without a value is null, a list is an array,
and a span is {start, stop} in character offsets, as Renyi's text
indices count (the Rust encoder converts its byte offsets). The Rust
encoder (crates/renyi_syntax/src/json.rs) produces the same document,
so the two front ends can be compared byte for byte; the previous
document (node keys, line and column positions) is gone, in one
change. The names of the Renyi types keep the derived document
unambiguous: no two sum types in the compiler's scope share a variant
name (IsValue beside the operator Is, the lexer's MemberName beside
the expression Member), no field is a reserved word (exposed for
exposing), and span is the last field of every node. (user)
W2. The front end written in Renyi lives in compiler/. The modules
are ast (the tree), lexer, parser, parse (the command line:
renyi run compiler/parse.ry [--declarations] <file> prints the tree)
and tokens (the token dump, as renyi tokens prints it); they are
named by their file stems and import one another by those names, since
the directory of the main file is the project root (J17). They are Renyi
programs like any other: renyi check clean, in canonical layout
(renyi format compiler/*.ry), within the size limits of V5, run by the
Rust VM. The parser's import ast exposing ... names every node type,
wider than any line, so the formatter breaks an exposing list wider
than the line after its commas, one name per line indented once, as V12
breaks a needs clause (reference section 16). (user)
W3. The judge is a Rust integration test.
crates/renyi/tests/selfhost.rs runs the Renyi parser on the VM over the
corpus, the conformance programs, the compiler's own sources and, with
--declarations, the library declarations, and compares its output with
renyi parse --json byte for byte; a program the Rust parser rejects
must make the Renyi parser fail. It runs in cargo test and in CI, so
the two front ends cannot drift apart. renyi parse --declarations
exists for it: a library declaration file's functions have no bodies.
(user)
W4. The lexer and the parser come first. The bytecode file format or
loader, and the performance items of docs/GAPS.md section 7 (in-place
collections, string building, a pattern cache), wait until the
self-hosted checker or emitter needs them or a measurement shows the VM
too slow; the parser's own run over the corpus is the first measurement.
(user)
W5. The checker is the next piece, transcribed from crates/renyi_check.
The checker written in Renyi lives in compiler/ beside the parser, one
module per Rust file where the Rust has one: lists, report (the
diagnostics, their rendering and the layout checks of renyi_syntax),
effects, suggest, types, refine, declare (world.rs: the
modules, types, functions, abilities and implementations of a project
and the library, resolved after every module is in), bodies
(check.rs: every function body, test, example, constant and refinement
condition, with the Rust checker's mutable state as a Checker record
threaded through every function) and the command line checker
(renyi run compiler/checker.ry [--json] [--strict] [--library <dir>]
<file>..., which reads the library's declaration files, the file and
its imports, and prints what renyi check prints). It is written in one
pass as a transcription, as the parser was, and committed as one piece;
every message and fix is the Rust checker's, verbatim. (user)
W6. The profile of the VM waits until the checker exists. Where the
3.9 s of the parser's run on parser.ry go is measured after the
checker is written in Renyi, so that one profile covers the parser's
and the checker's runs; the checker's run over the compiler's own
sources is the second measurement. (user)
W7. The judge of the checker is full equality with renyi check --json.
crates/renyi/tests/selfhost.rs runs renyi run compiler/checker.ry
--json over every program of the repository (the corpus, the
conformance programs and the compiler's own sources; the library
declaration files are not checkable programs) with and without
--strict, and compares the output with renyi check --json
[--strict] byte for byte (codes, severities, positions, messages, fixes
and order) and the exit status with the Rust one; a program the Rust
parser rejects must make the Renyi checker fail. It runs in cargo
test and in CI with the parser's judge. (user)
W8. The checker also produces the reference table the VM's compiler
needs. bodies.ry records what check.rs records for the VM: the
target of every name (Target: a function, an ability method, a
constant, a type, a variant, an ability), the kind of every number
literal (NumberKind), the context type of every call whose result the
context alone decides, whether every otherwise guards a fallible
value, and the literals and results noted while a body is checked
(Reference, SpanTy), as Renyi values keyed by span. They are
produced now and judged when the emitter exists, which is the piece
that consumes them. (user)
X. The speed of the VM (session 8)
X1. The VM is optimized in development builds. cargo test and CI
run the renyi binary of the development profile, where the parser
written in Renyi took 5 s on parser.ry and the checker 32 s on
bodies.ry; the release profile runs both four to five times faster
(1.0 s and 5.5 s). The workspace manifest sets opt-level = 3 for
renyi_vm and for every dependency in the development profile, so that
the judges, CI and every renyi run from a development build get the
release speed of the interpreter while the front end, the checker and
the binary keep their fast rebuilds. (user)
X2. slice(start, stop) on lists. The lexer written in Renyi cut a
token's text one character at a time (chars.at(index) and a text
concatenation per character: 12 percent of a checker run), and no
composition of the existing methods takes a middle run of a list in
time proportional to its length (take and drop copy from the
start). The prelude gains slice(self: List of Item, start: Integer,
stop: Integer) returns List of Item: the items from start to stop,
the end exclusive, the bounds clamped to the list as take and drop
clamp theirs; the sketch, library/std, the VM and the cheat sheet
change in one commit (a library addition, not a surface change under
V11). The lexer's slice is one slice and one join. Measured on the
checker's own 7500-line bodies.ry: 18 percent off the checker's run
and 17 percent off the parser's. (user)
X3. The interpreter loop is rewritten for speed before the emitter.
The profile (docs/GAPS.md, status at the end of session 8) shows the
time spread over the VM's plain operations, about 35 ns each: Load,
Field (a linear search of the record's field names on every access),
Binary, Call and Return, with every step returning a 48-byte
Result<Option<Value>, Interrupt> and every Value 40 bytes wide. The
owner chose the deep change over the three cheap ones measured
(arguments left in place at a call and one handler stack for all
frames, 5 to 8 percent; a cache of the field index, about 8 percent; a
fast path for is on records, about 3 percent): the dispatch loop
keeps the running frame's state in locals and returns nothing per
operation, Value is slimmed by boxing its wide variants, and the cheap
changes come with it. The target is 1.5 to 2 times on the parser's and
the checker's runs, measured with X4; the bytecode emitter waits for
it. (user)
X4. renyi run --profile. The VM carries its own profiler: a timer
raises a flag every half millisecond, the operation running when it was
raised gets the sample, every operation, call and library primitive is
counted, and the report (samples by function and operation, by
function, by operation kind with the primitives by name; the counts)
goes to the standard error when the run ends. It is a development aid
like renyi tokens, named in appendix B of the reference and in
05-agent-tooling.md. (user)
X5. The bytecode emitter writes a file. The compiler written in
Renyi will emit the VM's own instruction set (crates/renyi_vm/src/
bytecode.rs) into a file that renyi run loads and runs, rather than
hand its checked tree and references to the Rust compiler in one
process (a loader). The file format is the contract between the two
halves and must be kept equal to bytecode.rs; the Rust front end can
then be replaced without touching the VM, which is the point of
self-hosting (decision W2). The format and the loader are designed when
the emitter is written, after the residue of stage 1 (the owner's
order, below). (user)
X6. The binary allocates through mimalloc. The profile after X3
shows the remaining time spread over the plain operations, with every
record, list, text and frame going through the system allocator, which
is slow on Windows. The owner chose to add mimalloc as the global
allocator of the renyi binary (one dependency of C code, built by
cc on CI's Ubuntu and on MSVC; a decision in the territory of S1),
on the condition that it buys at least a tenth. Measured in the release
build, best of five, the two binaries run back to back: the parser on
parser.ry 811 to 496 ms, the checker on lexer.ry 510 to 389 ms, on
bodies.ry 2712 to 2138 ms (1.3 to 1.6 times); it stays. Asked in the
same batch: the development profile keeps the front end and the
checker unoptimized (X1 stands), and the residue of stage 1 (docs/
GAPS.md, section 7, the status of session 7) is finished before the
emitter is written. (user)
Y. The residue of stage 1 (session 8)
The owner's order after X6 was to finish what stage 1 of docs/GAPS.md
(section 7) left open before the bytecode emitter: entries 1.8, 1.14,
1.17 and 1.18 of the audit and the sketch's open item R3-2. Each was
read in full and the design choices asked as one batch of four.
Y1. A denied primitive that cannot fail crashes; the server fails.
Decision J11 reports a scope denial through the primitive's ordinary
error type, which a primitive without a failure type does not have:
filesystem.exists, environment.get and their kind crash when the
effective grant does not allow the call, and the crash names the
function whose needs narrowed the grant. This is now the rule (the
reference, section 11), not a gap: giving such primitives a failure
type would make every call to them take otherwise. The server joins
the modules that fail: serve outside the grant is
StartError.PermissionDenied(port), past a budget the new
StartError.OverBudget(port) (library/std/server.ry). Containment of
a path scope stays lexical, written into the reference: a scope
contains a path by its text once ., .. and the separators are
normalized, nothing is resolved on disk, so a symbolic link under the
scope reaches wherever it points. Canonical paths would cost a system
call per check and change the answer with the state of the disk, which
a replay cannot reproduce. (user)
Y2. A replay compares the recording's grant with the needs.
06-runtime-guarantees.md says a replay checks that the recording does
not exceed the grant it is replayed under, and the implementation
checked each recorded call's capability but never the grant header.
Now replay_with checks the header first, then the calls: every
capability the header names must be covered by the effective grant of
the test or program, else the replay is refused naming the capability
and the grant. The six fixtures of the corpus already fit their tests'
needs. (user)
Y3. Collections keep the derived Equal and Hash. A declared
equals decides is and is not (stage 1); Set items, Map keys,
distinct, to_set, contains and index_of compare and hash by
structure and consult no declared equals or hash, as the ability
table of the library sketch said and the reference now says (section
5). Consulting them would make a hash depend on user code and a
collection's behaviour on the implementation in scope, which the
derived form avoids; a type that wants its own notion of equality in a
collection keys it by a field. In the same entry: repeat, pad_left,
pad_right and rounded take a count that fits a machine word, and a
larger Integer is a crash, this Integer is too large for the
operation, written into the reference (section 7) rather than made a
failure. (user)
Y4. The loop unwinds its operands; a refinement sees its own field;
R3-2 waits. Entry 1.18 had two parts. (i) break or continue as
the outcome of an if or match expression nested in another
expression left the interrupted expression's operands on the stack: a
statement loop now marks the operand stack's height at its entry
(Op::MarkStack) and every break and continue drops the operands
above the mark (Op::UnwindStack) before it jumps, so the stack of a
long loop no longer grows and the emitter of X5 can assume a fixed
height at every loop boundary. (ii) The reference says a refinement is
a condition "over the field's name", and both checkers bound every
field of the record or variant: a field's condition now sees that field
alone (a subtype's value), and a condition that names another field
of the type is the error refinement-field, with the fix to check both
fields where the value is built; the condition code of the VM still
takes every field, unchanged. The sketch's open item R3-2 (constraints
with type arguments, for any Bag where Bag can Iterable of Item) is
deferred until after the emitter: it needs a design round of its own
and nothing in the compiler waits on it. (user)
Z. The bytecode file and the emitter (session 8)
Decision X5 set the emitter's shape: it writes a file the VM loads. What the file is, how the VM reads it, how the emitter written in Renyi is judged and where the commands go were asked as one batch of four, answered with the recommended options.
Z1. The bytecode file is the derived JSON of Renyi types. The
format is declared once, as the types of compiler/bytecode.ry
(Program, Code, Op and the rest, each can ToJson), and a
bytecode file is their derived JSON (library sketch, section 7), as the
syntax tree's JSON is the derived JSON of compiler/ast.ry (decision
W1): a record is an object whose keys are its fields in declaration
order, a variant an object with kind first, a maybe without a value
null, a list an array, a number a JSON number, and every span counts
characters. The first field is format, the version of the layout; the
extension is .ryc. The emitter written in Renyi builds the Program
value and prints it with json.render_indented; the Rust side
(crates/renyi_vm/src/file.rs) renders the same document from the VM's
own Program through the JSON writer of renyi_syntax, which the judge
of decision W3 already holds equal to Renyi's. A binary layout would be
smaller and faster to load, and would need a second definition of every
structure on each side; the JSON is read by any tool, diffed in a
review, and defined by the types the Renyi compiler needs anyway. The
maps of the VM's program (impls, method_index) are written sorted,
so that the document is a function of the program alone. Field names
that are reserved words of Renyi (function, module, returns,
fails, needs, set, ability, count, purpose) are renamed in
the file (function_id, owner or module_name, result,
failures, capabilities, set_type, ability_id, size,
summary), and a number constant is written as its digits in a string,
so that an Integer past a machine word and a Decimal's scale
survive the trip. (user)
Z2. The loader is hand-written over the VM's own JSON reader. No
serialization crate: file::load reads the document with
natives::json::read_json, the reader behind json.parse, and walks it
field by field into the VM's Program, refusing anything that does not
fit with the path of the place (the file.codes[3].ops[7].target: ...)
and refusing a format that is not the one this VM reads. After the
walk a consistency pass checks every index, a function's code, a
constant's code, an example's codes, a jump's target, a slot against
the code's locals, a constant against the code's table, a type, a
field site and a result type against the program's tables, so that a
file written by hand cannot send the VM past a table. This keeps the
dependency list at decision S1's (the reader exists already) and the
format readable by the Renyi compiler itself one day. (user)
Z3. The judge is byte equality, then behaviour. The emitter written
in Renyi is right when renyi run compiler/compile.ry <file.ry> --to
a.ryc writes, byte for byte, what renyi compile --to b.ryc <file.ry>
writes, over the corpus, the conformance programs and the compiler's own
sources, and when both refuse every program the Rust checker refuses
(crates/renyi/tests/selfhost.rs, as decisions W3 and W7 judge the
parser and the checker). Behaviour is judged by running: every run
case of the conformance suite runs a second time from the .ryc file
renyi compile writes for its program and must print the same output
and exit the same way (crates/renyi/tests/conformance.rs), the VM's
own test renders a program, loads it back, renders it again and runs
both (crates/renyi_vm/tests/file.rs), and the binary's test runs,
records and reproduces a program from its file
(crates/renyi/tests/compile.rs). Byte equality is the stricter judge
and the one that locates a difference; the reruns catch what a file
loses on the way (a span, a constant's digits, a fixture's path). (user)
Z4. renyi compile, and the file loads by its extension. renyi
compile [--to <file.ryc>] <file.ry> checks the program with its
imports and writes its bytecode, by default <name>.ryc in the working
directory, as renyi record names its recording; its diagnostics are
those of check, and a program with errors writes nothing. renyi
run, record, test and reproduce take a .ryc file wherever they
take a .ry file and load it instead of checking and compiling, so a
program's main, tests and examples run from the file alone; the
replays fixtures are found beside the source paths the file remembers.
The run manifest's code hash of a program loaded from a file is the
SHA-256 of the file's bytes, since the definitions main reaches are
not there to hash: a recording made from a .ryc reproduces from the
same .ryc. No separate load command and no --bytecode option: the
extension says what the file is. In the same entry: the spans a program
keeps, in process or in a file, count characters, as the syntax tree's
JSON does, so that a program loaded from a file is the program compiled
in process; the compiler converts the parser's byte offsets when it
emits, and a module's source is remembered as its path and the
character offsets of its line starts, which is what a crash location
and a test report need. (user)
AA. The rename of set (session 8)
AA1. set name to expression becomes change name to expression.
The owner asked whether to rename set to change and to forbid every
other way of changing a binding. The second half was already the rule:
the grammar has no =, a name is bound once per function (shadowing),
and set on a let mutable binding was the only way to change a value.
The rename stands on its own: set had three meanings, the statement,
the Set type and the Map.set method, and change x to x + 1 names
what happens to an existing binding. A surface change under decision
V11: this entry, then the reference (sections 1, 6, 8 and 17), the
grammar, the cheat sheet, the formatter, both front ends (Word::Change
and the statement Change of the tree and of its JSON; the reserved
list of compiler/lexer.ry, change_statement in compiler/parser.ry),
the checker's fixes (writechange x to x.method(...)`), the corpus,
the conformance programs, the compiler's own sources, the crates' tests
and the lint, in one commit.setleaves the reserved words andchangejoins them (88 words still);SetandMap.setare unchanged.
The readability rounds undertests/readability/` keep the grammar they
measured. (user)
AB. Constraints with type arguments (session 8)
AB1. A constraint takes the type arguments its ability declares, and
the collections are Iterable. Open item R3-2 of the sketch, deferred
by Y4 until after the emitter, asked as a batch of four and answered
with the recommended options. (i) for any Bag, Item where Bag can
Iterable of Item: a constraint names an ability with as many type
arguments as the ability declares (type-arity otherwise, with the fix
writeBag can Iterable of Item), and the arguments are any types in
scope: the clause's own parameters, concrete types and composites. An
ability's requirement follows the same rule with the ability's own
parameters in scope (`ability Countable of Item where self can Iterable
of Item`). Inside the body a parameter constrained to `Iterable of Item`
is walked by `for each` and the queries with items of type `Item`, and
a method of the ability called on it has the ability's parameters
substituted (`bag.to_list()` is a `List of Item`). At a call site the
argument's type must have the ability with matching arguments: those of
its implementation, with the implementation's parameters read as the
type's arguments (`Iterable of Item for Stack of Item` gives `Stack of
Text` the argument `Text`), or those a constraint of its own gives it;
matching binds what the constraint leaves open, so `Item` is inferred
from `Deck`'s `Iterable of Text`, and a mismatch is `missing-ability`
naming the arguments (totalneedsIterable of Integer, whichDeckdoes not have). (ii) The prelude implements Iterable for List of
Item, Set of Item, Map of Key to Value (its pairs), Range
(Integer) and Text (its characters), so that a generic walker
accepts a list and to_list exists on each; the loop's own rules for
the collections are these implementations now, an implementation head
takes full types as arguments (ability Iterable of Pair of Key, Value
for Map of Key to Value), a declaration file holds an implementation
with its methods' heads alone, and the VM dispatches an ability call on
a base value by its declared type. The alternative, a constraint that
only a declared implementation satisfies, would have refused a list to
every generic walker. (iii) An ability named without the arguments its
parameters call for, Bag can Iterable, is type-arity, the rule the
implementation head already followed; an item type left unknown would
have been an existential for little gain. (iv) One implementation of an
ability per type, whatever the arguments: a second is
duplicate-implementation, since dispatch is by the value's type alone
and a loop needs one item type; two implementations were accepted
silently before. In the same entry: an implementation's methods are as
visible as their ability, which the reference said and the checker did
not do (private-name on a public ability's method from another
module). A surface change under decision V11: this entry, then the
reference (sections 2, 3, 4, 5 and 8, appendix A), the grammar
(Implementation takes types after of; ImplDeclaration for a
declaration file), the cheat sheet, both front ends, the library and its
sketch, the conformance suite (four cases) and the checker's tests, in
one commit. (user)
AC. Packages (session 8)
AC1. Packages: a static registry, JSON manifests, the package name as
the first segment of an import, and the first slice of M4. The owner's
batch after the judges ran from bytecode, four questions answered with
the recommended options; the rules that carry them out are this entry.
(i) The registry is a directory or a URL, not a service:
<registry>/<name>/versions.json lists the versions,
<registry>/<name>/<version>/package.json holds the package's name,
version, purpose, dependencies, the toolchain that published it, every
source file's path with its SHA-256 and the effect manifest of Q1 (for
every public function, its transitive capabilities and failure types),
and the source files lie beside it. A package's content hash is the
SHA-256 of its package.json text, spelled sha256: and the hex digest
as the index spells hashes. A directory registry (absolute, or relative
to the project root) is read in place; a http:// or https://
registry is fetched into .renyi/packages/<name>/<version>/ under the
project root by renyi fetch and by renyi add, which check every
file's hash and recompute the effect manifest from the sources, refusing
a package whose claimed manifest differs: Q1's recomputation by the
registry is the client's until a registry with signatures exists (R7-2).
renyi publish [--to <directory>] writes a project into a directory
registry (a URL registry is published through its local copy): the
project must check clean, the version must be at least what the
semantic diff against the highest published version demands (G1: a new
major when a public signature went or changed, a new minor when one
came, else greater than the previous), and a published version is never
overwritten. (ii) The manifest is JSON. renyi.json in the project
root holds name, version, purpose, dependencies (package name to
the version required), registry and budgets (the thresholds of R7,
public_per_module, effect_paths_per_module and
fan_out_per_definition, which renyi index --budgets reads when they
are there); the lockfile renyi.lock.json beside it holds packages,
every package the program reaches with its name, version and
hash, sorted by name, written by renyi add and renyi update and
read by every other command. A project is the directory holding
renyi.json; a program's project root is the nearest such directory in
its file's directory or above it (up to the working directory for a
relative path), else the file's directory as before (J17), and its own
imports resolve from the root. A version is major.minor.patch; a
requirement names a version and means the same major and at least that
version; a program holds one version of a package, the highest one every
requirement allows, and requirements that disagree on the major are an
error. One format for the recording, the run manifest, the bytecode file
and the manifests, read by one reader (crates/renyi_json, the VM's
reader moved out); the alternatives were a package.ry in Renyi syntax
(a surface change under V11) and TOML (a new dependency). (iii) The
package name is the first segment. import <name>.<path> reaches the
dependency's file <path>.ry from the package's root and import
<name> its root module <name>.ry; inside the package a module header
is the path from the package's root (module fetch), and the checker
names the module <name>.fetch, the root module <name>; an import
inside a package that names one of the package's own dependencies
reaches that dependency, any other its own files; at the program level
an import reaches a package only when the manifest lists it as a
dependency. A dependency wins over a directory of the project with its
name; renyi add refuses such a name and std. No new keyword, and no
package name in the headers. (iv) The first slice is packages: the
manifest, the lockfile, renyi add <name> [<version>] (resolve, fetch,
print every public function's effects and failures, write both files),
renyi update [--accept-effects] (every dependency to the highest
version its requirement allows; one whose effects widen is refused
unless the flag is given and main's grant covers them), renyi audit
(every dependency's transitive effects against main's grant, and the
capabilities of the grant no dependency uses), renyi fetch, renyi
publish, the dependencies with their hashes in the run manifest, which
renyi reproduce compares (Q2), and the thresholds of R7 in the
manifest; std.process with the capability process is the next slice
and the foreign function interface (foreign, F0) the one after. Across
a package boundary the static coverage rule checks the kind of a
capability and the grant stack its scope, as reference section 11
already said, and the error names the package ("announce (package
greeting 1.0.0) needs console, which main does not declare"). A
package whose package.json differs from the lockfile is
package-mismatch on the import, a dependency of the manifest that the
lockfile or the registry lacks package-missing, and a manifest, a
lockfile or a package.json that cannot be read manifest-invalid. The
resolver is crates/renyi_package, used by the checker and the binary;
Bytes.sha256() joins the prelude so that the front end written in
Renyi verifies a package as the Rust resolver does
(compiler/project.ry), and the judges hold the two equal over the
projects with a dependency in the conformance suite
(tests/conformance/packages/). The front end written in Renyi reads the
manifests through std.json, whose maps keep one value per key, so a key
given twice is refused by the Rust resolver alone, as is a budget spelled
1.0 or 1e2 or a version part beyond 64 bits; the two are left to
differ there. (user)
AD. The diagnostics of the front end in Renyi (session 8)
AD1. The front end written in Renyi reports what the Rust front end
reports: every diagnostic with its code, message, span and fix, and the
same recovery. The "2" of the owner's answer "1+2" of 2026-10-07
(stage 3 and, beside it, this). Until this entry compiler/lexer.ry
and parser.ry stopped at the first problem with a message and an
offset, and the judges of crates/renyi/tests/selfhost.rs asked only
that both front ends reject the same programs; a toolchain in Renyi
that answers for renyi check and renyi compile must say what they
say on a broken file too. The rule: (i) the lexer and the parser in
Renyi carry every error site of crates/renyi_syntax/src/lexer.rs and
parser.rs with the code, the message, the span and the fix verbatim,
and recover where the Rust ones recover (the lexer reads on after a bad
character, which becomes an ErrorToken; the parser skips to the end
of the line after a bad import, statement, clause or example and to the
next item at the margin after a bad item, and keeps the partial tree);
the parser's Cursor carries the diagnostics and its failure Halt
carries the cursor, so that a caller that recovers reads them off it.
(ii) renyi run compiler/parse.ry prints the tree, then the
diagnostics as renyi parse --json prints them, and exits as it exits;
checker.ry and compile.ry print a rejected file's diagnostics as
renyi check and renyi compile do, and follow the imports of its
partial tree as the Rust resolver does. (iii) The three judges compare
the standard output and the exit status on every program, rejected
ones included (the emitter judge compares the files besides when both
wrote one); lane probes exercise the error sites no program in the
repository reaches, and two conformance cases hold a sample. (iv) The
rule of W7 extends to the front end: a new diagnostic, message or fix
in the lexer or the parser of crates/renyi_syntax is the same change
in compiler/lexer.ry or compiler/parser.ry in the same commit. The
alternative, one syntax diagnostic per rejected file, was what
existed: enough for the trees, not for the toolchain. (user)
AE. The process module (session 8)
AE1. std.process starts a program and waits for it: one call per
run, a status other than 0 a failure, the scope the program's name as
spelled, the parent's environment inherited. The second slice of
stage 3 (M4), after packages (AC1), in the order the owner chose. The
four questions of 2026-10-07 and their answers: (i) the shape of the
module: execute(program, arguments) and attempt(program, arguments)
run the program to its end and report a Completion (the status, the
standard output as text and as bytes, the standard error as text);
execute_with and attempt_with take an Options record (a working
directory, variables added to the environment, text for the standard
input, a time limit past which the program is killed and the call fails
with Timeout) and defaults() sets none; no handle, no interactive
reading or writing, no background process: one primitive call is one
record of a recording, and a handle would sit badly with tasks that run
one after the other (S2); a handle can be a later entry when a program
needs one. (ii) A status other than 0 is the failure Exited(program,
status, output, errors) of execute, so that a caller handles it like
any failure (as http.get fails on a status outside 2xx, K2);
attempt reports any status as a completion, for the programs whose
status is an answer (grep, diff). (iii) The scope process("git")
is matched by the program as the call spells it, text for text: the
rule J11 states ("a program name contains only itself"), the simplest
to explain, and the one under which a package's effect manifest says
exactly which programs the package may start; a full path needs a grant
of that path, and git.exe is not git. The alternative, the file
stem, would have let process("git") start any file so named anywhere.
(iv) The program inherits the parent's environment and working
directory; Options adds to or overrides them. A clean environment
would have made most programs (git wants HOME) need variables
passed by hand, for a capability the grant already limits by program.
The function is execute, not run: run is a reserved word (run
concurrently). The VM's boundary already took the first Text
argument of a process primitive as the scope of its effect and
counted its budget; the module adds the natives, the module's
ProgramNotAllowed and OverBudget, the tests, a conformance case,
and foreign alone stays unavailable (V6). (user)
AF. The foreign function interface (session 8)
AF1. A foreign module is a declaration file of the project that the
manifest binds to shared libraries; scalars, text and bytes cross the
boundary; the libraries load at run time through libloading and a call
goes through a fixed signature family; a foreign call is a primitive at
the boundary. The third slice of stage 3 (M4), after packages (AC1)
and std.process (AE1), in the order the owner chose; decisions F0 and
F1 named the capability and left the mechanism open. The four questions
of 2026-10-07 and their answers: (i) Bindings are declared, not written
in a new syntax (the surface is frozen, V11): a foreign module is a file
of bodiless public function declarations, each needs foreign and
nothing else, that the manifest's foreign section names ("foreign":
{"libc": {"library": ["ucrtbase", "libc.so.6", "libSystem.B.dylib"],
"symbols": {"renyi_name": "c_symbol"}}}): the libraries are tried in
order and the first that loads is used; symbols renames a function
whose C symbol is spelled otherwise. The resolver tags the file (the
main file by its path from the project root, an imported one by its
qualified name), the checker parses it as declarations and declares it
as a library module (its functions are primitives, recorded like the
library's), and renyi bind <header.h> --module <name> --library
<name>[,<name>...] [--to <directory>] writes such a file from a C
header's prototypes, with the manifest entry. The alternative, an
external clause on a function, was a surface change for what a file
and a manifest entry say as well. (ii) Only scalars, text and bytes
cross: the width types of std.foreign (Int8 to UInt64 and Size,
refinements of Integer), Float (a C double), Boolean, Text (a
NUL-terminated char *, copied each way) and Bytes (a pointer and a
length, two words, parameters only); a result is one of those but
Bytes, maybe Text for a char * that may be null, or nothing.
Records, lists, callbacks and ownership wait for a later slice; what the
first slice carries is what libc and libm take. The checker holds a
foreign module to it: foreign-signature, foreign-type and
foreign-arity (at most six words). (iii) The libraries load at run
time through libloading, a runtime dependency like those of S1, and a
call goes through a fixed family of signatures generated into
foreign_abi.rs by tools/gen_foreign_abi.py (every arity to six,
every pattern of integer-class and double-class words, three result
classes): the one unsafe corner of the VM; no libffi, no C compiler at
run time, and a C function that does not fit the family is reached
through a C wrapper that does. (iv) A foreign call is a primitive at the
boundary: a recording holds it with its result, a replay answers it
without calling, reproduce likewise; a guarded value (only to)
refuses to cross; foreign takes no scope and no budget (a budget
meters the world; a C call is the program's own code); renyi run
prints "this program can call native code through libc" when main
grants foreign (07-system-design.md, section 2.2); and renyi publish
refuses a project with foreign modules, since a package carries no
native code. capability-unavailable goes: nothing waits for M4 any
more. (user)
AG. Machine code for the bytecode (session 8)
AG1. The VM generates machine code for the bytecode through Cranelift
inside the one renyi binary, per code object once it is hot; the
bytecode file stays the contract between the front ends and the VM, the
compiler written in Renyi is untouched, and the interpreter remains the
reference: a frame whose typed assumptions fail at run time is handed to
it at the op in question. The owner's direction of 2026-10-07:
strengthen performance at the root, ahead-of-time compilation included.
The four options and the choice: (i) Cranelift in the process (chosen):
one binary, no toolchain at run time, and the same backend serves a later
renyi build; (ii) a C or LLVM backend: a compiler at run time, or a
second build pipeline; (iii) a faster interpreter alone: the loop of
decision X3 had reached 3.3 times slower than CPython on integer loops,
near the ceiling of a loop over boxed values; (iv) WASM: another target,
not a speed-up. How it works (crates/renyi_vm/src/native/): infer.rs
runs an abstract interpretation over a code object's stack bytecode and
settles which operands and slots stay unboxed (small Integers, Booleans,
Floats and the bounds of a range, in registers); codegen.rs translates
every op to Cranelift IR over that analysis, arithmetic, comparisons,
branches and range loops in registers and everything else as a call to a
runtime helper that does what the interpreter's arm does on the VM's
stack; runtime.rs holds those helpers and the hand-back (rt_deopt
boxes the registers into the interpreter's frame and operand stack at
the op). The generated code and the interpreter share every op's
implementation (Vm::op_*), so a change to an op is one change. The
conformance suite runs every program a third time with machine code for
every code object (RENYI_NATIVE_HOT=1), and crates/renyi_vm/tests/native.rs
covers the hand-backs: an Integer that leaves the machine word, a guarded
Integer in a typed parameter, a call past the depth of native frames,
failures, crashes and deadlines inside generated code. (user)
AG2. Integer stays arbitrary precision (decision B9): the generated
code computes on i64 with overflow checks and hands the op to the
interpreter, which has the big integers, when a result leaves the machine
word. The alternatives were a fixed-width Integer, a change to the
language, or big integers in the generated code, every arithmetic op a
call. A program that overflows a word at every op runs at the
interpreter's speed; one that never does runs in registers; the result is
the same either way. (user)
AG3. Machine code is on by default for run, record, test and
reproduce and so for the judges; --explain and --profile use the
interpreter, which narrates and counts; --interpret forces the
interpreter. A code object is compiled once the interpreter has run
HOT_FACTOR (2000) times its size in ops of it, and the machine code
takes over at the code's next call or, when the interpreter is inside
one of its loops, at the loop's next turn: the generated function has an
entry at every loop header whose operand stack holds nothing in
registers, which takes the frame's slots into registers under the same
checks as the parameters (a slot that does not fit leaves the frame
with the interpreter for good). Compiling an op costs a few hundred
times running it on the interpreter, so code run once or twice (a
driver, a setup, a short loop) never pays for machine code it would not
use, and a small script compiles nothing; the first rule, "a code
object with a loop at its first call", compiled 130 code objects of the
checker written in Renyi on examples/hello.ry for 0.13 s a run, most
of them loops of a few turns. A range of small Integers is iterated by
both tiers in the same shape (Native::RangeIterator: the next value,
the last and the step, no longer a list of its items), so a frame can
change hands inside such a loop. Measured on the development build:
examples/hello.ry starts in 46 ms either way; the front end written in
Renyi, compiling compiler/lexer.ry with every code object forced
native, generates code for 676 code objects (39,502 ops, 240,713
Cranelift instructions) in 0.42 s, three quarters of it Cranelift's own
register allocation and lowering. (user)
AG4. A bench/ directory with six benchmarks and tools/bench.py; CI
runs them and prints the numbers, it does not gate on them. The
programs: primes.ry (integer loops), strings.ry (text building and
character counting), records.ry (records in lists),
json_round_trip.ry (JSON rendering and parsing), the self-check of the
compiler written in Renyi (compiler/checker.ry on compiler/bodies.ry)
and the start of examples/hello.ry; the first four have CPython twins
next to them, and bench/*.ry are held to renyi check and the
canonical layout like compiler/. The targets the owner set: primes
at least five times faster than CPython (met: 84 ms against 529 ms on
the development build, 6.3 times), the compiler's self-check at least
twice as fast as before (not met by machine code; AG5), hello no
slower (met). (user)
AG5. What the machine code does not do, measured, and what follows:
the VM's own operations are the next performance work, bounded; an
ahead-of-time image (renyi build) and a baseline JIT that inlines the
boxed operations are planned after release 0.1. Measured 2026-10-07 on
the release build (Linux under WSL): the compiler written in Renyi
checking compiler/bodies.ry takes 1.6 s of CPU on the interpreter and
1.5 s on machine code for every code object, plus 0.36 s to generate it;
of its 5.4 million calls none hands a frame back. The time of such a
program is in the values, not in the dispatch: the profile (perf)
puts 32% in the interpreter's loop (of it 12% pushing values on the
stack), 14% in cloning values, 8% in dropping them, 10% in the allocator
(records and lists), 6% in the kernel zeroing fresh pages, 5% in pushing
frames and 4% in field access, and the generated code calls the same
runtime for all of it. So: (i) a bounded round of VM work on those paths
comes next (the stack push, the clone and drop of values, the allocation
of records, the call frame, the boundary of the pure primitives),
measured against bench/, after which positioning and release proceed
whatever the number (the owner's choice over "until the self-check is
twice as fast" and "no VM work now"); (ii) renyi build, an image of
the bytecode with the machine code of this machine that run loads in
place of generating it, is deferred until the machine code wins on
ordinary programs, since today it wins only on numeric loops, which
generate in milliseconds; (iii) inlining the boxed operations (loads,
stores, field access, calls) in the generated code, the road of a
baseline JIT, is deferred the same way. The development aids of the
backend: RENYI_NATIVE_REPORT prints what was compiled, the time per
phase and the hand-back and call counts on the standard error when the
run ends; RENYI_NATIVE_HOT sets the factor (0 compiles everything
at its first call and enters every loop at its first turn);
RENYI_NATIVE_OPT sets Cranelift's optimisation level (none by
default: the optimiser found nothing in helper-call code for twice the
time); RENYI_NATIVE_VERIFY turns Cranelift's IR verifier on. (user)
AG6. The bounded VM round of AG5, done and measured. Kept: a load of
a record or a variant that a read of one of its fields follows reads
the field in the slot; a frame names its grant by an index into a
table, a per-code flag says whether the function narrows the grant at
all, and the locals are filled in one resize; a variant without fields
is built once per type and tag. Tried and dropped: a store of a binding
fused with the load that follows it; the fields of a record or a
variant inline in its allocation. The interpreter's other operations
stay as they are, and the next performance work is the one AG5 defers
until after release 0.1. The owner's answer of 2026-10-07, on
learning that the machine code brings nothing to the compiler written
in Renyi: one bounded round of work on the VM's own operations, then
on to the next item whatever the number. The measure is the count of
user-space instructions and cycles (perf stat, the release build
under WSL, two binaries interleaved, the minimum of three rounds): the
loaded machine of that day moved wall and CPU time by a third between
two runs, the instruction count by nothing and the cycles by a few
percent. The program is the compiler written in Renyi compiling
compiler/bodies.ry on the interpreter. The whole round: instructions
from 15.49 billion to 14.66 billion (-5.4%), cycles from
10.71 billion to 10.27 billion (-4.1%); with the machine
code on, -2.5% and -4.5%. Step by step: (i) the fused field
read in its first form, with the store fusion, 2.0% of the
instructions; (ii) the grant by index, the flag and the resize, another
1.0%; (iii) the fused field read restricted to a slot that holds a
record or a variant, and the store fusion dropped, another 2.0%: the
look at the next op had cost every load and store something, and the
store fusion, which saved a pop and a push, took the record off the
slot, so the field read after it was the slow one again; (iv) the
inline fields (SmallVec<[Value; 3]> in Record and Variant, one
allocation in place of two), tried with the unit variants: 3.4% more
instructions and 0.8% fewer cycles on this program, 8.2% fewer
instructions and 0.6% fewer cycles on bench/records.ry, 0.4% more
and 3.0% more on bench/json_round_trip.ry, 2.2% more and 8.9% more
on a loop that builds a three-field record and reads it; the branch on
every field access and the element-wise build and drop cost what the
second allocation had cost, so the fields went back to a Vec and the
unit variants stayed. On the micro-benchmarks of the interpreter loop
(instructions per turn of the loop, before and after the round): a
counting loop, 713 to 731; a call and return, 1097 to
1100; a primitive call, 1857 to 1906; a field read,
1266 to 1133; a record built and read, 2099 to
1970 (the first three, which the round does not touch, move
with the code layout of the interpreter loop, which a build changes by
up to 3%, so a micro-benchmark alone attributes nothing). Not done,
with the reason: the boundary of a pure
primitive is a move of the arguments into the scratch buffer, a check
that no argument is guarded, a look at the function's needs (none)
and the call, and nothing in it is worth a second path; change x to x
with ... could move the receiver as change x to x.method(...) does
(decision X3) and update the record in place, but that rule is a change
to both emitters and the judges for 34 sites in compiler/, and waits;
a fused LoadField op would take the look at the next op out of the
loop, but an op the bytecode file never carries is a format question
first (decision Z1), and waits too. The time of such a program stays
where AG5 found it: in the clone and drop of values on every load and
return, in the stack traffic and in the allocator, which only the
baseline JIT or renyi build of AG5 can take out. (user)
AH. The niche (session 8)
AH1. Renyi is the scripting language of AI agents: the language an
agent writes and a person reviews at a glance; the program declares what
it may do, the runtime admits only that, and every run can be recorded,
replayed and narrated. Its first users are people who run automation
with Claude Code, Codex and their like and will not run an agent's
Python blind. The owner's second direction of 2026-10-07: the design
had its innovations but no fixed niche (decision V1 named "people
building agent workflows and learners" as the first users without saying
what the language is to them). The four candidates and the choice: (i)
the scripting language of agents (chosen): every guarantee the design
made (the effect system of sections C and P, the recorded runs of O and
P, the agent tooling of D and the project map, the packages of AC) is
what a person needs before letting an agent's program run; (ii) a
teaching language: the readability is real but the guarantees would be
a side show; (iii) a safe automation language for operations teams: the
same guarantees, but the author would be a person, for whom the English
syntax is slower to write than it is to read; (iv) a general-purpose
language with a safety story: nothing to lead with. The posture stays
general (AH4); the niche says who comes first. docs/design/08-positioning.md
holds the text. (user)
AH2. Four selling points, in this order: effects as capabilities,
declared on every function, scoped, budgeted and enforced at the
boundary, dependencies included; recorded, replayed and narrated runs;
the tooling an agent needs (the project map, the MCP server, purpose:
as syntax, a fix on every error); package effect manifests computed by
the tool and never widened silently. All four candidates offered were
taken; each is a promise the implementation keeps today (the conformance
suite, the judges, renyi audit), which is the condition for leading
with it. (user)
AH3. The English-like syntax ranks second: the positioning leads with the guarantees and shows the syntax as the means by which a person reviews a program at a glance. Leading with the syntax was the project's original pitch (decision A2) and reads as a curiosity; the guarantees are what the first users lack elsewhere. (user)
AH4. No non-goals are written; the posture stays that of a general language. A list of non-goals would read as limits before the language has users; the niche says who comes first, not who is excluded. (user)
AI. Release 0.1 (session 8)
AI1. Release 0.1 after the machine code, the positioning document and the release engineering land. The alternatives: release at once from the private repository, with nothing to install and nothing to read; or after the embedding API and the LSP of M5, months away. (user)
AI2. The release engineering, all of it: CI builds Linux, macOS and
Windows binaries on a tag and publishes a GitHub Release; an install
script and cargo install renyi (the crate name is free on crates.io,
checked 2026-10-07); a documentation site on GitHub Pages with the
reference, the cheat sheet, the examples and the positioning; a VS Code
extension for syntax highlighting. (user)
AI3. The first acquisition is a starter pack for agents: a skill file, the MCP server and five runnable workflow examples. The alternatives, a launch post or a benchmark table, say what the language is; the pack lets an agent use it in its first hour, which is where the niche of AH1 is won or lost. (user)
AI4. The name stays; a GitHub organisation renyi-lang takes the
repository as renyi-lang/renyi; the owner buys renyi-lang.org. (user)
AI5. The crates are self-contained: renyi_check embeds a copy of
library/std/ under crates/renyi_check/library/std/, and renyi a
copy of docs/cheatsheet.md as crates/renyi/cheatsheet.md; a test in
each crate holds the copy equal to the canonical file, byte for
byte. A crates.io tarball carries only the files under the crate's
directory, so an include_str! of a file outside it compiles in the
workspace and fails from the tarball, which cargo package
--workspace showed on 2026-10-07. The alternatives: the canonical
library/std/ moved under the crate (every path in the documents,
the tests, the judges and the lane scripts changes, and the cheat
sheet still needs a copy); or no crates.io for 0.1. The copies cost
copying the file again after an edit, which the tests demand; the
canonical paths stay. (user)
AJ. Foreign packages: Rust and Python (session 8)
The owner's question of 2026-10-07, evening: can a Renyi program use Python or Rust packages directly, so that the language extends without a library of its own for everything? Four answers.
AJ1. Rust packages extend Renyi through the mechanism the standard
library uses: a native function registered with the VM under a
declaration file that states its signature, its capability and its
failure types, built into the renyi binary; the registration API is
the half of the embedding API of M5 (decision Q3) that faces the
host. A registered native is checked, recorded, replayed and narrated
like a library primitive, so an extension costs no guarantee; the
standard library's own natives move to the same mechanism, which makes
the API tested by its first user. The alternatives: the C FFI alone
(AF1: a Rust crate as a C-ABI library through renyi bind, with
scalars, text and bytes only, and foreign outside every guarantee),
or nothing until a user asks. (user)
AJ2. Python packages are reached through a typed bridge to a worker
process: a declaration file states each function's signature over the
types JSON carries (numbers, text, booleans, lists, maps, records with
FromJson and ToJson), the VM starts one python process per run
and sends each call as a JSON message, and each call is one primitive
at the boundary, recorded with its arguments and its result. Not
CPython in the process: it opens a hole that can do anything, needs
libpython found at run time and the GIL handled, for a speed a script
does not need. Not std.process alone: one process per call, text
only, the types decoded by hand. The bridge keeps recording, replay,
reproduce and --explain; what the Python side does is outside
Renyi's guarantees, and the reference says so where it says it of
foreign (07-system-design.md, section 4.3). The bridge is an
extension package written against the API of AJ1, the first after the
standard library. (user)
AJ3. A call through the bridge charges one capability, python,
scoped by the package the declaration file binds: needs
python("pandas"). A reviewer reads in the signature that the program
runs Python and which package. The alternatives hide that: effects
declared per function by the binding's author are a promise the checker
cannot verify, and foreign does not tell a C library from Python. A
new capability kind is a change to the reference's section 11 and the
checker's table, not to the surface frozen by V11. (user)
AJ4. The order: after release 0.1, the registration API of AJ1 first, then the bridge of AJ2 and AJ3, before the other items of M5. The alternatives, before 0.1 or after M5, either delay the release or leave the ecosystem question open while the first users arrive. (user)
AK. The registration API (session 8)
The owner's answers of 2026-10-07, late evening, to the four questions
decision AJ1 left open. The API is crates/renyi_vm/src/extension.rs,
the guide docs/extensions.md, the test
crates/renyi_vm/tests/extension.rs.
AK1. An extension is built into a binary by a crate that depends on
renyi as a library: renyi::main_with(vec![EXTENSION]) is the whole
program of such a binary, and the official renyi is
main_with(Vec::new()). The crate renyi is a library and a binary;
an extension is an ordinary Rust crate that depends on renyi_vm for
the value types and holds an Extension value; a user builds a renyi
with it in a crate of three lines and cargo install --path .. The run
manifest names the extensions a run had beyond the standard library
(extensions, each name version), reproduce warns when they differ,
and renyi version lists them. The alternatives: Cargo features of the
renyi crate, which would make every extension a dependency of this
repository and leave a third party to fork it; or loading an extension
as a shared library at run time, which Rust's unstable ABI would reduce
to the C family of AF1, without lists, maps and records. (user)
AK2. A declaration file and its natives meet by name: an Extension
names its modules with their declaration files and a table of Native
entries, each a module, a function name, the type of the function's
first parameter as the checker spells it, in full (List of Integer) or
by its head (List), or no receiver, and the Rust function;
Registry::verify holds the table and the declarations equal both ways,
and a binary with an extension that fails it does not start. A lookup
takes the entry whose receiver is the full spelling, then the head, then
the entry without one. The standard library is the first extension
(natives::standard()): the eleven lookup functions under natives/
became the same keys as data, two hundred entries, and the two-way check
passed on them at once, which is the test of the API that decision AJ1
asked for. The alternative, a procedural macro reading the Rust
signature and writing the declaration, would move the place a reviewer
reads the capability and the failures into Rust source, for machinery
the table does not need. (user)
AK3. A native of an extension sees what the standard library's natives
see: the Vm, the plain Values of its arguments and the helpers of
renyi_vm::natives (arg, text, int, small, list, map,
Vm::library_record, Vm::fail_variant and the rest); NativeFn is
the one signature. The standard library's two hundred natives are the
proof that it is enough. A typed layer (FromValue, IntoValue, a Rust
signature per Renyi signature) can be laid over it later without
changing it; it is not needed to write an extension. (user)
AK4. An extension declares its capabilities from the kinds of
reference section 11 and adds none: console, filesystem, network,
environment, time, random, process and foreign, with python
to come by AJ3; a new kind is a decision and a change to the reference
and the checker's table. The reviewer's vocabulary does not grow with
the extensions a binary carries, and a native declared under a kind
that takes a scope gets the scope check, the budgets and the guards of
the boundary from its Path or Url parameter without code of its own.
A denied call or one past a budget is reported through the variant of
the function's declared failure type named PermissionDenied,
HostNotAllowed or ProgramNotAllowed, or OverBudget, with the scope
as its one field (the first argument when the capability carries none,
as serve names its port): the rule the library's modules followed by
name, now followed by shape, so that an extension's own error type is
reported the same way; a function without such a variant crashes, as
before. The alternative, kinds registered by extensions, would make the
grant's names depend on the binary. (user)
AL. The Python bridge (session 8)
The owner's answers of 2026-10-07, late evening, to the four questions
decision AJ2 left open. The bridge is crates/renyi_vm/src/natives/python.rs
with the worker python_worker.py beside it, the checker's rules
crates/renyi_check/src/python.rs, the guide docs/python.md, the
tests crates/renyi/tests/python.rs and the fixtures
tests/conformance/python/ and python_bad/.
AL1. A Python module is bound as a foreign module is (decision AF1): a
declaration file of the project, written by hand, that the manifest's
python section names: "python": {"interpreter": "...", "modules":
{"analysis": {"package": "analysis", "symbols": {"mean": "average"}}}},
interpreter, package (the Python import name; the module's own name
when left out) and symbols optional. Each function of such a file
needs python("<package>") and nothing else, fails with PythonError
and nothing else and takes no type parameters (python-signature); its
parameters are types that can ToJson and its result a type that can
FromJson, or nothing (python-type): what std.json renders and
parses is what crosses, records and sum types included, and the
checker's structural rule decides it. The bytecode file carries the
binding per function (python after foreign; the file is format 3).
The alternatives: declaration files shipped with the toolchain for
known packages, which would make the toolchain answer for the shape of
every Python package it names and go stale with each release of one; or
declaration files generated from Python's annotations, which most
packages do not carry and which say nothing about failures. (user)
AL2. The protocol is one JSON object per line on the worker's
standard input and standard output: {"call": N, "module": "...",
"function": "...", "arguments": [...]} and {"call": N, "result":
...} or {"call": N, "error": {"where": "import" | "call" | "result",
"kind": "ValueError", "message": "..."}}, the arguments by position in
the order the declaration lists them; one worker per run, started at the
first call with the project root on its module path, ended with the
VM. The worker's program is a text the VM passes with -c, so a
project installs nothing; what the Python functions print goes to the
standard error, so it cannot mix with the protocol; a replay never
starts the worker; a result the declaration does not mention is
dropped. The alternatives: a socket or a pipe pair with a framing of its
own, more machinery for the same messages; or one process per call
(std.process), which pays an interpreter start per call and loses the
module-level state between calls. (user)
AL3. The interpreter is the manifest's interpreter when given, else
the environment variable RENYI_PYTHON when set, else python3 and
then python on the PATH (python first on Windows, where python3
is usually the Store's stub); the first that starts and answers the
bridge's greeting is kept for the run. A project that needs a
particular environment (a virtual environment, a version) names it in
the manifest, next to the modules that need it; a machine-wide choice
goes in the variable; a plain setup needs nothing. The alternative, the
PATH alone, would make a project's virtual environment a matter of the
shell the program was started from. (user)
AL4. One failure type for the bridge, PythonError of std.python:
Raised(exception, message) for an exception the function let escape
(its class name and its text), NotCarried(detail) for a result the
declared type does not fit or that JSON does not carry,
Unavailable(detail) for an interpreter that does not answer, a module
that does not import, a function that is not there or a worker that
ended, and PermissionDenied(package) for a package outside the grant
(the rule of AK4); python takes a scope and no budget. A declaration
cannot say which exceptions a Python function raises, so one variant
carries them all with the class name to match on; the other cases are
the bridge's own. The alternative, a failure type per declaration file,
would be written by hand for every package and checked by nothing.
(user)