Skip to content

One way to learn Aether, and main's red Linux leg: the path, batch 9's examples, a glossary, Rosetta Code, #3432 and #3481 - #3477

Merged
nicolas-maman merged 14 commits into
mainfrom
docs/oct30-learning-path
Oct 30, 2026
Merged

nicolas-maman merged 14 commits into
mainfrom
docs/oct30-learning-path

Conversation

@nicolas-maman

@nicolas-maman nicolas-maman commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

One way to learn Aether: a single path from installing it to whole programs, the examples batch 9 left out, a learner's glossary, a first tranche of Rosetta Code tasks, and #3432.

Scope

Fixed at the start. Anything found on the way that is outside it is filed for the next batch, unless it is a security, crash or data-loss problem.

Found on the way and filed for the next batch, not fixed here: #3478 (an if value whose condition ends in a call does not parse) and #3479 (c = s[i] warns that its type was left to codegen). Three Rosetta tasks wait for them instead of being written around them.

Reviews

Two rounds, then stop:

  1. One fresh review of the whole diff, fixed in d9dec8a. The main finding was that a make buffer has one owner that releases it. The path taught it as a handle to free, in the new glossary and tutorial and in four existing examples (11-arrays-and-slices, 17-ownership, 03-ask-reply's no-reply wording among them), and nine examples freed one by hand. One way to learn Aether: getting-started, tutorial.md, tutorials/ and examples/ teach the same things four times #3475's target, "no claim the runtime does not keep", covers fixing the old examples too. The round also fixed:
    • the deadline wording in 07-limits;
    • a constant-time HMAC check;
    • a TCP line reader that keeps what follows a newline;
    • the tutorial's overclaims;
    • a syntax quick reference.
  2. One review of those fixes, fixed in 06e8b78: the slice rule's exceptions (a buffer in a field, a var, a tuple, a message or a captured local is the program's to free), the Rosetta counts the removed frees changed (Aether 450 lines now), and smaller wording.

Found outside the scope and filed for the next batch: #3478, #3479 (compiler; three Rosetta tasks wait for them), #3480 (three reference statements the review found wrong), #3483 (a timing-bound runtime test that failed on macOS ASan and Valgrind here; re-run), #3485 (a refused binary import leaves its stub directory), #3486 (an unused function in ae that clang -Werror rejects).

Checks

Local, Windows x86_64 (MSYS2 MINGW64), on 06e8b78:

  • make check-examples: 93 examples, 84 built, 81 run and matched, 0 problems. Ten whole runs in a row were clean before the Rosetta tranche. Each new example ran 30 of 30 alike, and each changed one 5 of 5.
  • make check-docs: 121 complete blocks compile, 198 run and match.
  • make check-changelog, make check-tests and ae fmt --check on every changed file pass.

#3481: main's red Linux leg, fixed here

Main's Linux / GCC leg has been red since the 0.861.0 release, in the cross-version ABI matrix, and so was this PR's. The fix is here too: one PR, as asked; it was #3482 for an hour, now closed.

The cause:

  • The matrix prints the failing program's stderr now (012c9a4). It showed "the library was made for Aether runtime 0.860.0/abi1 and runs on 0.863.0/abi1", then a panic outside any try.
  • An ae build program is linked -rdynamic, so on ELF a library's calls into the runtime it carries bound to the program's runtime.
  • Across versions, the library's runtime ABI check (Discussion: compiler/runtime ABI compatibility policy for shipped native libraries #2957) refused, and the program ended.
  • macOS and Windows keep the two copies apart and pass.

The owner chose that model for Linux and FreeBSD too (682c917):

  • ae links a library with its own runtime -Bsymbolic on ELF.
  • The import refusal of what needs one runtime, and the warning that a library's actors are not drained at exit, now apply everywhere. Both name --shared-runtime.
  • An older ELF library of another runtime ABI, linked without -Bsymbolic, is refused at import, naming the rebuild, instead of ending the program. The flag is read from its dynamic section.
  • After one review (b65fb05), three more changes. A static-runtime library whose exports pass a value holding strings, whose builders use their config, or which runs actors names an own_runtime duty in its catalog. An importer of 0.860.0 to 0.864.0, which treated ELF's copies as merged, therefore refuses it instead of letting the two runtimes free each other's values. Cross-built ELF libraries are linked -Bsymbolic too. And Linux keeps a test of the interposed path, which hand-linked libraries can still take (sandbox_nested_libraries 2f).
  • The review of those fixes found the duty missing for some shapes ae refuses and named for some it imports. That is fixed in the last commit:
    • The compiler and ae now read the catalog's own spellings through one header (compiler/aether_lib_strings.h), so they decide alike. It also finds a string inside a tuple, which ae let cross before, and visits each struct once.
    • The duty is named for ELF only (the C names it under __ELF__), so importers of 0.860.0 to 0.864.0 on macOS and Windows keep refusing per module and warning about actors, as before.
    • --shared-runtime with --target is refused: a cross-built library carries the target's runtime, while its catalog said the runtime was shared.
    • binary_import_ownership and binary_import_actors check which libraries name the duty.

The tests follow:

  • the ABI matrix checks the SYMBOLIC link and the refusal;
  • binary_import_actors, binary_import_ownership, binary_import_structs and sandbox_nested_libraries expect on Linux what they expect on macOS and Windows;
  • emit-lib.md, abi-policy.md and ownership-model.md say so.

On Windows ae builds with -Werror, and it compiles for x86_64 Linux with zig. The ELF reader was checked on x86_64, aarch64 and 32-bit ARM libraries built with and without -Bsymbolic. Linux and FreeBSD behaviour is CI's to show. The review of the fixes was the last one: its findings are fixed here, and no further review round runs.

Found by this PR's CI and fixed here

The first macOS ARM64 run to finish failed in check-examples. In 07-limits, clang warned that spin(), which returns an int and ends in while true, does not return a value, and the warning reached the example's output.

The cause is on main. Under --emit=lib the safe point at each loop head leaves the loop when a deadline or a cancel ends the call. The C then fell off the end of a function that returns a value, so the value the export's wrapper read was undefined.

Such a function now returns its result's zero value at its end, as the wrapper does. emit_lib_deadline gains a case that compiles it with -Werror=return-type and checks the call returns 0 at its deadline. That case failed before the fix, under GCC too.

Closes #3432, closes #3475, closes #3476, closes #3481

#3432 asked that a root's pathLenConstraint count, as OpenSSL and Go
count it. validate_chain does already: since the rewrite for #3303 the
anchor joins the path before path_code runs, and path_code checks every
CA from index 1 to the end, the anchor last. Nothing tested it.

crypto_tls13_chain gets a PKI made with openssl 3.6: rootpl0, a
self-signed root with pathLenConstraint 0; icapl, a CA under it, with
plleaf under icapl; and pldirect, an end entity of rootpl0. openssl
verify refuses plleaf ("path length constraint exceeded") and takes
pldirect, and so do the two new cases. A root without the extension is
the existing "the same leaf under its root".

Negative control: with path_code's loop stopped before the anchor
(while j < n - 1), "a CA below a pathLenConstraint 0 root" is accepted
and the test fails; restored, all 39 checks pass. path_code's comment
now says the anchor is checked, so the loop is not trimmed.
The path stopped before batch 9. Six new examples, each with its output
checked by make check-examples (30 of 30 runs alike for each, and ten
whole check-examples runs clean):

- 02-actors/11-ending.ae: stop, kill and a panic, and the reason a
  monitor's Down carries; a send to an ended actor is counted.
- 02-actors/12-links.ae: a failure climbs links to an actor that traps
  exits; a "normal" stop ends nobody.
- 02-actors/14-bounded-mailbox.ae: @mailbox(bound: 3), try_send's three
  answers, and a plain ! past the bound. The actor fills its own mailbox
  from its handler, where it can take nothing, so the counts are exact on
  every run; main's sends to it raced the cores (18 of 30). main prints
  with print, which flushes, so the runtime's one stderr report of a drop
  lands where a reader sees it.
- 02-actors/15-streams.ae: std.stream holds a producer to a window of 3
  over 20 lines; the peak in flight is the window.
- 03-std/10-tcp.ae: a server and a client in one program, reading the
  byte stream up to the newline that ends a message.
- 03-std/11-cryptography.ae: SHA-256 (the standard "abc" vector), an
  HMAC, an Ed25519 key, signature and check, all pure Aether; the public
  key in the Output block is the one openssl derives from the same seed.
- 04-interop/07-limits: a C host runs script.ae's exports under a 50 ms
  deadline, a cancel from another thread, and a 64 KiB execution account
  (10 of 10 runs alike).

Links come before the supervisor built on them, so 12-supervisor.ae is
13-supervisor.ae and 11-sharded-map.ae is 16-sharded-map.ae, with every
reference moved (Makefile, the cache benchmark, three docs, and
10-crash-isolation's pointer). The path README links the tutorial and the
glossary, and the result-type anchor it and 10-errors.ae use had two
hyphens where GitHub's slug has three.
Getting started, docs/tutorial.md, docs/tutorials/01-03 and examples/
taught hello, control flow and actors four times, in four orders, and
partly in an older style: getting started taught the deprecated `export`
keyword and modules that are std.fs now, the tutorials answered with
chains of print calls, and nothing linked docs/tutorials/.

Each now has one job:
- getting-started.md installs, runs a first program, starts a project,
  and covers the tools, versions, other targets and troubleshooting; its
  language sections are gone (the path teaches them), and the details of
  publishing release artifacts and binary packages moved to
  package-fetching.md, which pointed back at them;
- the examples are the path;
- tutorial.md is the path told in prose: a chapter per part, why each
  step comes where it does, common mistakes, and exercises with answers;
  its 11 programs with output are run and compared by make check-docs;
- docs/tutorials/ is folded into it and removed.

The README's documentation list starts with the path, in order. The FIFO
regression test quoted the old tutorial; it quotes the new one.
#1604's third item: the words the tutorial, the examples and the
reference use, each defined in a few lines with where to read more:
actor, actor_ref, ask, borrowed, bounded mailbox, handle, link, safe
point, sum type and the rest. The path, the tutorial and getting started
link it. Its anchors were checked against the headings they name.
#1191)

examples/08-rosetta/ holds 22 classic tasks as Rosetta Code states them:
FizzBuzz, Ackermann, Fibonacci (negative n too), factorial, GCD, the
sieve, Hanoi, N queens (1 to 10, 92 for eight), string reversal by
grapheme, palindromes, Roman encoding, binary digits, quicksort, binary
search, hailstone (27 has 112 terms; 77031 the longest below 100,000),
Levenshtein, happy and perfect numbers, look-and-say, Josephus, Pascal's
triangle, and prime decomposition of 2^100 - 1 with std.bignum. Each is a
whole program, formatted, with no warning, its Output block checked
(make check-examples: 93 examples, 0 problems), its values those the task
states.

The path's README gives each task's length beside the first Go and
Python solutions in RosettaCodeData (1d475861): non-blank lines that are
not only a comment, Aether's header and Output block left out. Over the
22, Aether 455, Go 642, Python 233; the README says the other languages'
first files are not always whole programs. The Aether counts were
recounted, and the Go FizzBuzz count checked against the data.

Roman numerals/Decode, Leap year and Caesar cipher are left out rather
than written around two compiler bugs, filed for the next batch: #3478
(an if value whose condition ends in a call does not parse) and #3479
(c = s[i] warns, its type left to codegen). The issue stays open for the
rest of Rosetta Code. The tutorial's last chapter points at the tasks.
cpu_recommend_cores caps the default at 16 (MAX_CORES), and the cores follow the CPUs the process may use; the entry said one per CPU.
…#3432)

One fresh review read every change against the code. Fixed here:

- A make buffer has one owner, the local that releases it; the glossary
  and the tutorial taught it as a handle the program frees, as did
  11-arrays-and-slices and 17-ownership on the path, and five path
  examples and four Rosetta ones freed one by hand. The tutorial now gives
  three rules (counted values, a slice's buffer, handles), the examples
  say and do what the runtime does, and 11-arrays-and-slices' Output
  names the line its panic moved to.
- 03-ask-reply said an ask without a reply waits out the 5-second
  timeout; it ends at once, "the handler returned without replying".
  The glossary's ask entry says both.
- 07-limits: aether_set_call_deadline(ms) sets one instant, ms from then,
  not a budget per call; host.c and script.ae say so, and sleep_ms is
  valid for a second or more.
- 11-cryptography checks an HMAC tag with ct.bytes_eq_slice, not == on
  hex strings, and says why.
- 10-tcp keeps what follows a newline for the next read (a LineReader
  struct), checks every error, plays both sides on one thread instead of
  blocking a core in a handler, and calls TCP the layer under HTTP (std.http
  does not use std.tcp).
- The tutorial promised exercises in every chapter (the first two have
  them; README and getting-started said the same), told a newcomer to give
  blocking work "its own actor" (std.worker is the answer), claimed a
  host never sees a crash and that Go runs in-process, said conditions
  "take" no parentheses, and that reading an actor's state is impossible
  rather than racy. The syntax quick reference the old tutorials ended
  with is back, at the end.
- Glossary: link (a normal end ends nobody), safe point (--emit=obj too,
  and the JSON parse), counted value, outcome, contrib.
- 11-ending: three common ways an actor ends, not "the first of three";
  14-bounded-mailbox, 15-streams and 07-limits use for and break where
  they hand-rolled loops and a flag.
- getting-started: the aether.toml ae init writes, word for word; a
  missing pthread.h is libc6-dev (build-essential), not
  libpthread-stubs0-dev. bootstrap-from-source.md and cic-help.md
  described the old getting-started.
- The Rosetta caveat cited Go's leap year, a task the tranche leaves out;
  the changelog counted six examples, not seven.
- crypto_tls13_chain asserts the refusal's reason, so a rotted fixture
  refused for something else fails.

Outside the diff and filed for the next batch: #3480 (the reference's
reply section, the stdlib reference's constant-time advice, and
emit-lib's deadline wording).

check-examples (93, 0 problems), check-docs, check-changelog, check-tests,
the chain test and ae fmt --check pass; the changed examples ran 5 of 5
alike (07-limits 3 of 3).
The second and last review, of d9dec8a alone, found:

- The slice rule had no exceptions: a buffer put in a struct's field, a
  module var, a tuple or a message, or held by a local a closure
  captures, is not released for the program (language reference, "Slice
  ownership"; memory-management.md). 17-ownership, the tutorial's rule and
  its common mistake, and the glossary now say so, so a reader who stores
  a buffer in a struct frees it.
- Removing the frees changed four Rosetta line counts (sieve 21,
  N queens 48, Levenshtein 26, Josephus 26) and the total (450).
- The in-process Go host is a Go library built as a C shared library,
  not a TinyGo one.
- Chapter 2 opened with "nothing outside an actor touches its state",
  which its own common mistake now contradicts; it says other code
  reaches an actor through messages.
- 15-streams stops on any offer other than 1 (-1 after finish too).
- 10-tcp sends with write_all, since one write may send part of a
  buffer.
- The tutorial's counted values include sum values; std.tcp is the
  layer HTTP runs on, not a layer under std.http; the standard library
  follows two of chapter 1's rules; the path README says only the first
  two chapters have exercises.

check-examples (93, 0 problems), check-docs and check-changelog pass,
and the changed examples ran 5 of 5 alike.
…3481)

Main's Linux / GCC leg aborts both Aether-consumer cases of the matrix
since 0.861.0, and the log shows only "Aborted (core dumped)": run_app
sent the program's stderr to app.err and never printed it. A failed case
now prints the last lines of what the program wrote there.
#3481)

Main's Linux / GCC leg failed the cross-version ABI matrix since 0.861.0,
both Aether-consumer cases aborting. The stderr the previous commit
prints:

  aether: the library was made for Aether runtime 0.860.0/abi1 and runs
          on 0.863.0/abi1: rebuild it with the Aether of the runtime it
          runs on
  aether: panic outside any try/catch or actor: ...

An `ae build` program is linked -rdynamic (std.dl and script_gateway
plugins resolve runtime symbols against it), so on ELF it exports its
runtime, and a library's calls into the runtime it carries bound to the
program's instead. The library's code ran on a runtime it was not made
for; across versions the runtime ABI check of #2957 says so and refuses,
and the importer ended on the panic. Equal versions (0.860.0 against
0.860.0) and a library older than the check (against 0.859.0) passed,
which is why it showed only once main moved past the release.
macOS (two-level namespace) and Windows (PE) keep the copies apart and
pass, and docs/abi-policy.md says a self-contained library carries its own
runtime.

The owner chose that model for Linux too. So:
- ae links a library with a runtime of its own -Bsymbolic on ELF: its
  references bind to what it defines, and it still exports them for a C
  host. A --shared-runtime library defines no runtime and is unchanged.
- The import refusal of what needs one runtime (a struct holding strings,
  a builder using its config, #2931) and the warning that a library's
  actors are not drained at exit apply on every platform; both name
  --shared-runtime, as on macOS and Windows.
- A library of another runtime ABI linked before this (no DT_SYMBOLIC or
  DF_SYMBOLIC in its dynamic section, read from the file) is refused at
  import on ELF, naming the rebuild, instead of ending the program on its
  first call. The reader was checked on x86_64, aarch64 and 32-bit ARM
  libraries built with and without -Bsymbolic.

Tests: the ABI matrix checks that this ae links SYMBOLIC on ELF, expects
the refusal for an old library linked without it, and still runs the old
importer with this library (which now keeps its own runtime);
binary_import_actors, binary_import_ownership, binary_import_structs and
sandbox_nested_libraries expect on Linux and FreeBSD what they expected
on macOS and Windows. Docs: emit-lib.md, abi-policy.md and
ownership-model.md.

Checked here: ae builds with -Werror on Windows and compiles for x86_64
Linux with zig (-Wall -Wextra -Werror); the scripts parse. Linux and
FreeBSD behaviour is CI's to show.
@nicolas-maman nicolas-maman changed the title One way to learn Aether: the path, batch 9's examples, a glossary, Rosetta Code, and #3432 One way to learn Aether, and main's red Linux leg: the path, batch 9's examples, a glossary, Rosetta Code, #3432 and #3481 Oct 30, 2026
# Conflicts:
#	tests/integration/abi_matrix/test_abi_matrix.sh
One review of the two #3481 commits found:

- An importer of 0.864.0 or before took ELF's runtime copies as merged:
  it never refused an import that needs one runtime there, so with a new
  library keeping its own runtime, a struct holding strings or a builder
  using its config would cross unrefused and the two runtimes free each
  other's values, and a library's in-flight actor messages be dropped
  unwarned. A static-runtime library with such an export, builder or
  actors now names the `own_runtime` duty in its catalog's `requires`;
  importers since catalog 1.11 refuse a duty they do not know, and this
  ae performs it. Built here: a struct-holding export and an actor
  library name it, a plain one and a --shared-runtime one do not.
- Cross-built ELF libraries (ae_cross.c, the Tier-A and FreeBSD links)
  are linked -Bsymbolic too, so a --target rebuild keeps its runtime as
  the refusal message promises; build-system.md says so.
- The ELF reader needs the whole header of its class, a named byte order
  and a real program-header count (not PN_XNUM), leaves an empty dynamic
  segment unknown rather than "not symbolic", and keeps offsets within
  fseek's range.
- sandbox_nested_libraries keeps Linux coverage of the interposed path of
  #3438 (still reachable by libraries linked without -Bsymbolic): case 2f
  links the two libraries by hand from aetherc's C, checks they are not
  SYMBOLIC, and runs them under the -rdynamic host.
- A library's failure still reaches a `try` around the call (the import
  stub raises it again, #2956): emit-lib.md, app_panic.ae and the
  changelog said a panic never reached the program's catch.
- The script gateway's docs (module.ae, the header and .c comments, the
  stdlib reference) said a POSIX script binds to the host's runtime; a
  default-built script keeps its own on Linux and FreeBSD now, as on macOS.
- Stale comments and docs: emit-lib.md (the copies stay apart, its test
  table), containment-sandbox.md, the Makefile's shared-runtime note,
  ae.c's binary-import link note, aether_sandbox.h, and
  emit_lib_failed_result's app.ae. The matrix notes when it cannot check
  the -Bsymbolic link.

Not done: a CI step pinned to an older release, which the review proposed
on the belief that 0.864.0 was published; the latest published release is
0.860.0, so the existing step already crosses runtime ABIs.

Checked here: the build with -Werror; ae.c and ae_cross.c compile for
x86_64 Linux with zig (-Wall -Wextra -Werror); check-api-docs,
check-docs, check-changelog, check-tests; binary_import_structs,
binary_import_ownership, binary_import_actors, abi_policy,
emit_lib_accounts, sandbox_nested_libraries, and the ABI matrix against
the 0.860.0 Windows release, all passing on Windows. Linux is CI's.
The review of b65fb05 found the duty missing for shapes ae refuses (a
builder passing a struct with strings, a struct reaching one through a
typed-pointer field, a @c_callback export) and named for shapes it
imports (a struct of closures). The compiler and ae now decide from the
catalog's own spellings through compiler/aether_lib_strings.h, which also
finds a string inside a tuple (ae let one cross before) and enters each
struct once.

The duty is named under __ELF__ only: importers of 0.860.0 to 0.864.0 on
macOS and Windows kept the copies apart and refuse per module and warn of
actors themselves, as before. --shared-runtime with --target is refused,
since a cross-built library carries the target's runtime. The ELF reader
refuses a program header offset past LONG_MAX up front. The ownership and
actors tests check which libraries name the duty; the docs list it.
…ero value

Found by this PR's first macOS ARM64 run to finish: check-examples saw
clang's -Wreturn-type warning for 07-limits' spin() in the example's
output. Under --emit=lib the safe point at each loop head leaves the loop
when a deadline or a cancel ends the call, and the C then fell off the end
of a function that returns a value (undefined for the export's wrapper).
A body whose last statement is a loop that cannot complete now returns its
result's zero value at its end, as the wrapper does; a closure result is a
zeroed _AeClosure. emit_lib_deadline compiles such functions with
-Werror=return-type and checks the call returns 0 at its deadline (failed
before, under GCC too).
@nicolas-maman
nicolas-maman marked this pull request as ready for review October 30, 2026 23:34
@nicolas-maman
nicolas-maman merged commit 5eaa882 into main Oct 30, 2026
50 of 51 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment