From 332263cb293d23fc5598333dfeaea391123339e6 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 15:36:44 -0300 Subject: [PATCH 01/13] test: a trust anchor's own pathLenConstraint holds (#3432) #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. --- ...61030T120000Z-fixed-3432-anchor-pathlen.md | 7 +++++ std/cryptography/tls13_cert/module.ae | 4 ++- .../crypto_tls13_chain/test_tls13_chain.ae | 26 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 new_changelogs/20261030T120000Z-fixed-3432-anchor-pathlen.md diff --git a/new_changelogs/20261030T120000Z-fixed-3432-anchor-pathlen.md b/new_changelogs/20261030T120000Z-fixed-3432-anchor-pathlen.md new file mode 100644 index 000000000..cdb4fb4c0 --- /dev/null +++ b/new_changelogs/20261030T120000Z-fixed-3432-anchor-pathlen.md @@ -0,0 +1,7 @@ +- **A trust anchor's own pathLenConstraint is pinned by a test (#3432).** + Path validation has checked the anchor as it checks every CA since the + rewrite for #3303, so a root with pathLenConstraint 0 allows no CA below + it, as on OpenSSL and Go. Nothing tested it: crypto_tls13_chain now has a + real root with pathLenConstraint 0, refused with a CA below it and taken + with an end entity directly under it, and the anchor check, taken out of + the path loop, fails the first case. diff --git a/std/cryptography/tls13_cert/module.ae b/std/cryptography/tls13_cert/module.ae index 758ca0c9b..20be76595 100644 --- a/std/cryptography/tls13_cert/module.ae +++ b/std/cryptography/tls13_cert/module.ae @@ -3270,7 +3270,9 @@ fn names_allowed(cx: *ChainCtx, ca: *LeafCert, c: *LeafCert) -> int { } // The checks over the whole candidate path (leaf at 0, anchor last): path -// length constraints and name constraints. 0 if they hold. +// length constraints and name constraints. 0 if they hold. The anchor is +// checked as every CA is: a root's own pathLenConstraint holds, as on +// OpenSSL and Go (#3432). fn path_code(cx: *ChainCtx) -> int { n = list_size(cx.path) j = 1 diff --git a/tests/integration/crypto_tls13_chain/test_tls13_chain.ae b/tests/integration/crypto_tls13_chain/test_tls13_chain.ae index e7fcdadee..8b39b0208 100644 --- a/tests/integration/crypto_tls13_chain/test_tls13_chain.ae +++ b/tests/integration/crypto_tls13_chain/test_tls13_chain.ae @@ -49,6 +49,10 @@ // with a leaf (akikeyidleaf and so on); akiself, whose AKI // names its own serial, and akiselfleaf // pinned a self-signed leaf (peer.example, cA FALSE), its own anchor +// rootpl0 a self-signed root with pathLenConstraint 0 (#3432); icapl, +// a CA under it, and plleaf (pl.example) under icapl; pldirect +// (direct.example), an end entity of rootpl0. OpenSSL refuses +// plleaf ("path length constraint exceeded") and takes pldirect. import std.list import std.string @@ -196,6 +200,22 @@ fn pem_pinned() -> string { return "-----BEGIN CERTIFICATE-----\nMIIBezCCASGgAwIBAgIFUEVFUgEwCgYIKoZIzj0EAwIwFzEVMBMGA1UEAwwMcGVl\nci5leGFtcGxlMCAXDTI2MDEwMTAwMDAwMFoYDzIxMjYwMTAxMDAwMDAwWjAXMRUw\nEwYDVQQDDAxwZWVyLmV4YW1wbGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARv\n7S9eWT57ZTmAOztQD2+5EetBRQ+B+cOtLJCr7BVBQPQExOwo/jrT1ahb6i7ZxObZ\nFrq4KWW801ggO2EZ0SqXo1gwVjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH\ngDAXBgNVHREEEDAOggxwZWVyLmV4YW1wbGUwHQYDVR0OBBYEFH8HRengm5cRK0K4\na6DbxP8afdpfMAoGCCqGSM49BAMCA0gAMEUCIDCIjkFoXJK1RI1twupNRu+WcP5M\nrU1MCAQ84vwQWP4tAiEArDbfJRioR7QSSuE4iKxVCCFaHbJs/7eLq3CNJGUFXoM=\n-----END CERTIFICATE-----\n" } +fn pem_rootpl0() -> string { + return "-----BEGIN CERTIFICATE-----\nMIIBfzCCASWgAwIBAgICNDIwCgYIKoZIzj0EAwIwJDEiMCAGA1UEAwwZQ2hhaW4g\nVGVzdCBSb290IHBhdGhMZW4gMDAgFw0yNjAxMDEwMDAwMDBaGA8yMTI2MDEwMTAw\nMDAwMFowJDEiMCAGA1UEAwwZQ2hhaW4gVGVzdCBSb290IHBhdGhMZW4gMDBZMBMG\nByqGSM49AgEGCCqGSM49AwEHA0IABJixxflMvkDeua89RNt+KArzeGyQ8hnlDH5R\nZzYiY4d1I8FzEJQcjKPycZdFCYuNXyWuYKA9Uv0r6jKReOZsU0ijRTBDMBIGA1Ud\nEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQ6eD+1J16X\n591e4CrL6gUi9T0IKTAKBggqhkjOPQQDAgNIADBFAiAgLUCT6/LgwwoQVWaz36t6\nvn+1oYTXOfqE/QJbvtNS3QIhAOhrdUCU+O/CmfCOSq6kWcsVpaPAR4i7qI1vIAfq\nJwH+\n-----END CERTIFICATE-----\n" +} + +fn pem_icapl() -> string { + return "-----BEGIN CERTIFICATE-----\nMIIBqzCCAVGgAwIBAgICNDMwCgYIKoZIzj0EAwIwJDEiMCAGA1UEAwwZQ2hhaW4g\nVGVzdCBSb290IHBhdGhMZW4gMDAgFw0yNjAxMDEwMDAwMDBaGA8yMTI2MDEwMTAw\nMDAwMFowMjEwMC4GA1UEAwwnQ2hhaW4gVGVzdCBJbnRlcm1lZGlhdGUgdW5kZXIg\ncGF0aExlbiAwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7lexepBf+j1EAG3B\n/tgknaUIaLHGqs2IUhMkdsLoDGzuPz6vXC3HXySXx2W8isGJBPQ8bO6TwRCKDdXv\nJb20/KNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O\nBBYEFIY5FRWcWrq5LFmAPNRfeeNKxChSMB8GA1UdIwQYMBaAFDp4P7UnXpfn3V7g\nKsvqBSL1PQgpMAoGCCqGSM49BAMCA0gAMEUCIQCCiwWO4z8kDvDN1Hg+qtv98jXT\nb3VG1ClmrjXfOXv0/AIgRDHoEgB2agcKCOWt5KKJK1nng1PqyT6IHeUCQ+0w5xY=\n-----END CERTIFICATE-----\n" +} + +fn pem_plleaf() -> string { + return "-----BEGIN CERTIFICATE-----\nMIIByDCCAW2gAwIBAgICNDQwCgYIKoZIzj0EAwIwMjEwMC4GA1UEAwwnQ2hhaW4g\nVGVzdCBJbnRlcm1lZGlhdGUgdW5kZXIgcGF0aExlbiAwMCAXDTI2MDEwMTAwMDAw\nMFoYDzIxMjYwMTAxMDAwMDAwWjAVMRMwEQYDVQQDDApwbC5leGFtcGxlMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAETLmE9IQVKOAXDeR/uW3/dYF5h1iAbLzEAJrF\n9q+7TN4Feqkar7uzboHVkLuZ2EMo8QONDx4QwU4+hnWvDxR/paOBjTCBijAMBgNV\nHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAV\nBgNVHREEDjAMggpwbC5leGFtcGxlMB0GA1UdDgQWBBRzQKvkW9RPKr7oZD5gN8nR\nVAFpMjAfBgNVHSMEGDAWgBSGORUVnFq6uSxZgDzUX3njSsQoUjAKBggqhkjOPQQD\nAgNJADBGAiEAttcVwI+fd1u3CbS8ggb3tCofR7eMR+kJlA3vdX6SNfgCIQCE3n2y\nX0QPc/8H31I4eWzyx+b2cT/fztIrU6BhS+mUkQ==\n-----END CERTIFICATE-----\n" +} + +fn pem_pldirect() -> string { + return "-----BEGIN CERTIFICATE-----\nMIIBwjCCAWegAwIBAgICNDUwCgYIKoZIzj0EAwIwJDEiMCAGA1UEAwwZQ2hhaW4g\nVGVzdCBSb290IHBhdGhMZW4gMDAgFw0yNjAxMDEwMDAwMDBaGA8yMTI2MDEwMTAw\nMDAwMFowGTEXMBUGA1UEAwwOZGlyZWN0LmV4YW1wbGUwWTATBgcqhkjOPQIBBggq\nhkjOPQMBBwNCAAR6QCQbdqF6/klw7YKA6egcCMDJIXHpMZSaxnkpGsoLiKDYHNSF\nz5pZOIIDs1hMmZTTgOaopEMzz3wZLCDWv5+Po4GRMIGOMAwGA1UdEwEB/wQCMAAw\nDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBkGA1UdEQQSMBCC\nDmRpcmVjdC5leGFtcGxlMB0GA1UdDgQWBBQHZsheJCEETMGvixypCTuNM6RswzAf\nBgNVHSMEGDAWgBQ6eD+1J16X591e4CrL6gUi9T0IKTAKBggqhkjOPQQDAgNJADBG\nAiEAr8lcqOA0THyGFeI7/keW4m7IiDLv8/0bIQ2okBNHDWACIQCaG8jwlGhx4gC4\nPJGN04x/Qgqj8fVjjpDFBhipcMPFdA==\n-----END CERTIFICATE-----\n" +} + fn pem_icabadku() -> string { return "-----BEGIN CERTIFICATE-----\nMIIBnTCCAUKgAwIBAgIJAO5sOGJd9DW0MAoGCCqGSM49BAMCMBoxGDAWBgNVBAMM\nD0NoYWluIFRlc3QgUm9vdDAgFw0yNjAxMDEwMDAwMDBaGA8yMTI2MDEwMTAwMDAw\nMFowJjEkMCIGA1UEAwwbQ2hhaW4gVGVzdCBJQ0EgYmFkIGtleVVzYWdlMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAE+AT3F9m1t43Wl9KNC+yZsRppa8bDFl4kbaiG\nPsGxKk7l0lYq8gIUpwiyFW+NNS6VqczNLtuv9B4bwTJq1IeE96NjMGEwDwYDVR0T\nAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAQCAQYwHQYDVR0OBBYEFHxFROCF4R/hDlD3\nhc31z5wKhT7eMB8GA1UdIwQYMBaAFJMTFLsV1/bq649cPms99xshFSNtMAoGCCqG\nSM49BAMCA0kAMEYCIQCaWu+7dE/IqZF7nMyC1p94Ii6rWRmTVyp/NllSkM+V3gIh\nAMUI5Zc9uRt4FhKk4Yp+eKa/vEKhlA2ln7/mgL91HabB\n-----END CERTIFICATE-----\n" } @@ -369,6 +389,12 @@ main() { fails = fails + check("a root whose AKI names its own serial", anchor_result(pem_akiself(), pem_akiselfleaf(), ""), 1) fails = fails + check("the same leaf under its root", anchor_result(pem_root(), pem_leaf(), pem_ica()), 1) + // A trust anchor's own pathLenConstraint holds, as on OpenSSL and Go + // (#3432): a root with pathLenConstraint 0 allows no CA below it. The + // root without the extension is "the same leaf under its root" above. + fails = fails + check("a CA below a pathLenConstraint 0 root", anchor_result(pem_rootpl0(), pem_plleaf(), pem_icapl()), 0) + fails = fails + check("an end entity of a pathLenConstraint 0 root", anchor_result(pem_rootpl0(), pem_pldirect(), ""), 1) + // A pinned self-signed leaf with basicConstraints cA FALSE: trusted as // it stands (it has no issuer), as OpenSSL trusts it. fails = fails + check("a pinned self-signed leaf (cA FALSE)", anchor_result(pem_pinned(), pem_pinned(), ""), 1) From e9c1e806bef4ddfb8da563d89c1c4a800710d28a Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 15:58:27 -0300 Subject: [PATCH 02/13] examples: what batch 9 added, on the path (#3476) 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. --- Makefile | 2 +- .../concurrent_cache_bench.ae | 2 +- docs/design/concurrent-cache-benchmark.md | 4 +- docs/design/sharded-actor-map.md | 2 +- docs/structured-concurrency.md | 4 +- examples/01-language/10-errors.ae | 2 +- examples/02-actors/10-crash-isolation.ae | 2 +- examples/02-actors/11-ending.ae | 77 ++++++++++++++++ examples/02-actors/12-links.ae | 81 +++++++++++++++++ .../{12-supervisor.ae => 13-supervisor.ae} | 2 +- examples/02-actors/14-bounded-mailbox.ae | 83 +++++++++++++++++ examples/02-actors/15-streams.ae | 88 +++++++++++++++++++ .../{11-sharded-map.ae => 16-sharded-map.ae} | 2 +- examples/03-std/10-tcp.ae | 77 ++++++++++++++++ examples/03-std/11-cryptography.ae | 55 ++++++++++++ examples/04-interop/07-limits/host.c | 75 ++++++++++++++++ examples/04-interop/07-limits/run.sh | 14 +++ examples/04-interop/07-limits/script.ae | 51 +++++++++++ examples/README.md | 23 +++-- ...030T130000Z-added-3476-batch-9-examples.md | 11 +++ 20 files changed, 640 insertions(+), 17 deletions(-) create mode 100644 examples/02-actors/11-ending.ae create mode 100644 examples/02-actors/12-links.ae rename examples/02-actors/{12-supervisor.ae => 13-supervisor.ae} (97%) create mode 100644 examples/02-actors/14-bounded-mailbox.ae create mode 100644 examples/02-actors/15-streams.ae rename examples/02-actors/{11-sharded-map.ae => 16-sharded-map.ae} (97%) create mode 100644 examples/03-std/10-tcp.ae create mode 100644 examples/03-std/11-cryptography.ae create mode 100644 examples/04-interop/07-limits/host.c create mode 100644 examples/04-interop/07-limits/run.sh create mode 100644 examples/04-interop/07-limits/script.ae create mode 100644 new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md diff --git a/Makefile b/Makefile index 923756e44..9767e0679 100644 --- a/Makefile +++ b/Makefile @@ -2281,7 +2281,7 @@ self-test: compiler @echo "Running Compiler Self-Test" @echo "===================================" @echo "Testing compiler on complex syntax..." - @./$(BUILD_DIR)/aetherc$(EXE_EXT) examples/02-actors/11-sharded-map.ae $(BUILD_DIR)/test_compile.c && \ + @./$(BUILD_DIR)/aetherc$(EXE_EXT) examples/02-actors/16-sharded-map.ae $(BUILD_DIR)/test_compile.c && \ echo "✓ Complex syntax compilation successful" @echo "" @echo "Testing collections..." diff --git a/benchmarks/concurrent-cache/concurrent_cache_bench.ae b/benchmarks/concurrent-cache/concurrent_cache_bench.ae index e0c5afd53..66ad6a432 100644 --- a/benchmarks/concurrent-cache/concurrent_cache_bench.ae +++ b/benchmarks/concurrent-cache/concurrent_cache_bench.ae @@ -12,7 +12,7 @@ // 2. SHARDED ACTOR MAP — lock striping, actor-native: N owner actors, // key routed by hash(key) % N, so unrelated keys hit different // mailboxes. Fire-and-forget writes to distinct shards drain in -// parallel. (examples/02-actors/11-sharded-map.ae) +// parallel. (examples/02-actors/16-sharded-map.ae) // 3. COW SNAPSHOT CELL — std.snapshot. One atomic pointer to an // immutable map; reads are LOCK-FREE acquire loads with NO mailbox // round-trip; a write rebuilds the map and publishes with store(), diff --git a/docs/design/concurrent-cache-benchmark.md b/docs/design/concurrent-cache-benchmark.md index 09fe2976e..89d69e1c6 100644 --- a/docs/design/concurrent-cache-benchmark.md +++ b/docs/design/concurrent-cache-benchmark.md @@ -20,7 +20,7 @@ clock (`os.now_monotonic_ns`) and printing **ops/sec**. | # | Design | Read path | Write path | Source | |---|--------|-----------|------------|--------| | 1 | **Single-owner actor** | `?` ask to the one owner | `!` to the one owner | the "one mutex" trap (`docs/design/sharded-actor-map.md`) | -| 2 | **Sharded actor map** | `?` ask to `shards[hash(key)%N]` | `!` to `shards[hash(key)%N]` | `examples/02-actors/11-sharded-map.ae` | +| 2 | **Sharded actor map** | `?` ask to `shards[hash(key)%N]` | `!` to `shards[hash(key)%N]` | `examples/02-actors/16-sharded-map.ae` | | 3 | **COW snapshot cell** | lock-free `snapshot.load`, no mailbox | rebuild the whole map, `snapshot.store`, defer-free the displaced snapshot by one generation | `std.snapshot` / `docs/design/snapshot-cell.md` | **Workload mixes** (each runs a fixed 100 000-op budget over 64 keys): @@ -148,5 +148,5 @@ three designs, not artifacts of one machine. design, shard-count guidance, and the Zipfian skew caveat. - [`docs/design/snapshot-cell.md`](snapshot-cell.md), the COW cell, the reclamation contract, and when read-mostly is the right call. -- [`examples/02-actors/11-sharded-map.ae`](../../examples/02-actors/11-sharded-map.ae), +- [`examples/02-actors/16-sharded-map.ae`](../../examples/02-actors/16-sharded-map.ae), the canonical sharded-map example the benchmark's design 2 mirrors. diff --git a/docs/design/sharded-actor-map.md b/docs/design/sharded-actor-map.md index b4e89ebd2..bcd686653 100644 --- a/docs/design/sharded-actor-map.md +++ b/docs/design/sharded-actor-map.md @@ -13,7 +13,7 @@ readers and writers without collapsing onto a single core. [Design discussion on the issue tracker](https://github.com/aether-lang-dev/aether/issues/839). See the runnable example at -[`examples/02-actors/11-sharded-map.ae`](../../examples/02-actors/11-sharded-map.ae) and +[`examples/02-actors/16-sharded-map.ae`](../../examples/02-actors/16-sharded-map.ae) and the regression test under [`tests/integration/sharded_actor_map/`](../../tests/integration/sharded_actor_map/). diff --git a/docs/structured-concurrency.md b/docs/structured-concurrency.md index f354aed23..eb3515109 100644 --- a/docs/structured-concurrency.md +++ b/docs/structured-concurrency.md @@ -151,7 +151,7 @@ Strategies mirror Erlang: `one_for_one`, `one_for_all`, `rest_for_one`; restart kinds `permanent`, `transient`, `temporary`; past `max_restarts` within `within_secs` the supervisor kills its children and ends with a reason that says why, so a supervisor above it takes the failure. See -std/supervise/README.md and examples/02-actors/12-supervisor.ae. +std/supervise/README.md and examples/02-actors/13-supervisor.ae. ## Phase 2, capability-scoped concurrency @@ -231,7 +231,7 @@ crypto-miner actor because it cannot name one. Extend existing `hide`/`seal except` denial to concurrency sites. 6. **Docs and worked example.** Done for Phase 1: `docs/actor-concurrency.md` ("The delivery contract", "The end of an - actor", "Supervisors") and examples/02-actors/12-supervisor.ae. The + actor", "Supervisors") and examples/02-actors/13-supervisor.ae. The sealed subtree waits for Phase 2. Phase 1 shipped without Phase 2. Phase 2 can ship independently, they diff --git a/examples/01-language/10-errors.ae b/examples/01-language/10-errors.ae index 8d407a11b..8b9f1bcd9 100644 --- a/examples/01-language/10-errors.ae +++ b/examples/01-language/10-errors.ae @@ -13,7 +13,7 @@ // forced open are panics too. // // Run: ae run examples/01-language/10-errors.ae -// See: docs/language-reference.md#the-t-result-type-and-or--sugar +// See: docs/language-reference.md#the-t-result-type-and-or---sugar import std.string diff --git a/examples/02-actors/10-crash-isolation.ae b/examples/02-actors/10-crash-isolation.ae index cd6a8a2f9..715893470 100644 --- a/examples/02-actors/10-crash-isolation.ae +++ b/examples/02-actors/10-crash-isolation.ae @@ -8,7 +8,7 @@ // does. Crashes stay where they happen. Another actor learns of one // through `actors.monitor` (a `Down` message) or `actors.link`, and // std.supervise restarts a crashed actor under the same ref -// (12-supervisor.ae). An ask is how a caller finds out on its own: +// (13-supervisor.ae). An ask is how a caller finds out on its own: // `v, err = actor ? Msg {}` ends at once with "the actor is dead" for a // dead actor, and with "the actor died before it replied" for one that // dies handling the ask. diff --git a/examples/02-actors/11-ending.ae b/examples/02-actors/11-ending.ae new file mode 100644 index 000000000..726c2eb45 --- /dev/null +++ b/examples/02-actors/11-ending.ae @@ -0,0 +1,77 @@ +// Ending an actor, and what its watchers are told. +// +// An actor ends once, by the first of three things: it stops itself +// (`actors.stop()` in its own handler, reason "normal"), something kills it +// (`actors.kill(ref)`, from anywhere, reason "killed"), or its handler +// panics (the panic's message is the reason). An actor that monitors it +// (`actors.monitor(ref)`, from a handler) gets one `Down { ref, reason }` +// when it ends. A message sent to an actor that has ended is dropped and +// counted, never delivered. +// +// Run: ae run examples/02-actors/11-ending.ae +// See: docs/actor-concurrency.md#the-end-of-an-actor + +import std.actors + +message Work { n: int } +message Finish {} +message Watch { who: actor_ref, label: string } + +actor Worker { + receive { + Work(n) -> { + if n < 0 { + panic("negative work: ${n}") + } + println("worker: did ${n}") + } + Finish() -> { actors.stop() } + } +} + +actor Watcher { + state name = "" + receive { + Watch(who, label) -> { + name = label + _id = actors.monitor(who) + } + Down(ref, reason) -> { println("watcher: ${name} ended: ${reason}") } + } +} + +watch(who: actor_ref, label: string) { + w = spawn(Watcher()) + w ! Watch { who: who, label: label } +} + +main() { + a = spawn(Worker()) + b = spawn(Worker()) + c = spawn(Worker()) + watch(a, "a") + watch(b, "b") + watch(c, "c") + wait_for_idle() + + a ! Work { n: 1 } + a ! Finish {} // a stops itself + wait_for_idle() + + actors.kill(b) // b is killed from outside + wait_for_idle() + + c ! Work { n: -1 } // c's handler panics + wait_for_idle() + + a ! Work { n: 2 } // a has ended: dropped, and counted + wait_for_idle() + println("sends to ended actors: ${actors.dead_sends()}") +} + +// Output: +// worker: did 1 +// watcher: a ended: normal +// watcher: b ended: killed +// watcher: c ended: negative work: -1 +// sends to ended actors: 1 diff --git a/examples/02-actors/12-links.ae b/examples/02-actors/12-links.ae new file mode 100644 index 000000000..52b211227 --- /dev/null +++ b/examples/02-actors/12-links.ae @@ -0,0 +1,81 @@ +// Links: a failure takes down the actors linked to it. +// +// `actors.link(ref)`, from a handler, ties two actors both ways. When one +// of them ends, the other ends too, with the same reason, unless it traps +// exits (`actors.trap_exits(true)`): then it gets a `Down` with the reason +// and goes on. A stop's reason, "normal", ends nobody. So a failure climbs +// a chain of links until it reaches an actor that traps exits. The +// supervisor in the next example is built that way. +// +// Run: ae run examples/02-actors/12-links.ae +// See: docs/actor-concurrency.md#links + +import std.actors + +message LinkTo { other: actor_ref } +message Parse { text: string } +message Finish {} +message Ping {} + +// Parses what it is sent, and panics at input it cannot read. +actor Parser { + receive { + Parse(text) -> { + if text == "}" { + panic("cannot parse '${text}'") + } + println("parser: read '${text}'") + } + Finish() -> { actors.stop() } + } +} + +// Owns a parser through a link: if the parser fails, so does the session. +actor Session { + receive { + LinkTo(other) -> { actors.link(other) } + Ping() -> { reply "alive" } + } +} + +// Traps exits: a linked actor's end reaches it as a Down, and it goes on. +actor Guard { + receive { + LinkTo(other) -> { + actors.trap_exits(true) + actors.link(other) + } + Down(ref, reason) -> { println("guard: its session ended: ${reason}") } + Ping() -> { reply "alive" } + } +} + +main() { + // guard <-> session <-> parser + parser = spawn(Parser()) + session = spawn(Session()) + guard = spawn(Guard()) + session ! LinkTo { other: parser } + guard ! LinkTo { other: session } + wait_for_idle() + + parser ! Parse { text: "{" } + parser ! Parse { text: "}" } // the parser panics, the session ends with it + wait_for_idle() + println("guard after the failure: ${guard ? Ping {}}") + + // A parser that stops normally ends nobody linked to it. + parser2 = spawn(Parser()) + session2 = spawn(Session()) + session2 ! LinkTo { other: parser2 } + wait_for_idle() + parser2 ! Finish {} + wait_for_idle() + println("session 2 after its parser stopped: ${session2 ? Ping {}}") +} + +// Output: +// parser: read '{' +// guard: its session ended: cannot parse '}' +// guard after the failure: alive +// session 2 after its parser stopped: alive diff --git a/examples/02-actors/12-supervisor.ae b/examples/02-actors/13-supervisor.ae similarity index 97% rename from examples/02-actors/12-supervisor.ae rename to examples/02-actors/13-supervisor.ae index 5cb3e57b2..b6a11323a 100644 --- a/examples/02-actors/12-supervisor.ae +++ b/examples/02-actors/13-supervisor.ae @@ -7,7 +7,7 @@ // child alone; past its restart budget it gives up and ends itself, and // whoever monitors it gets a Down that says why. // -// Run: ae run examples/02-actors/12-supervisor.ae +// Run: ae run examples/02-actors/13-supervisor.ae // See: std/supervise/README.md, docs/actor-concurrency.md#supervisors import std.actors diff --git a/examples/02-actors/14-bounded-mailbox.ae b/examples/02-actors/14-bounded-mailbox.ae new file mode 100644 index 000000000..cdbe4fa2e --- /dev/null +++ b/examples/02-actors/14-bounded-mailbox.ae @@ -0,0 +1,83 @@ +// A bounded mailbox: what an actor will hold, and what a sender is told. +// +// A send never blocks its sender, so an actor's mailbox has no bound: what +// it has not handled yet waits in memory. `@mailbox(bound: N)` before the +// actor makes it take at most N messages it has not handled yet. Past +// that a send is refused at once, never waited on: +// - `try_send(ref, Msg { ... })` sends as `!` does and says what happened: +// "" when it sent, "the mailbox is full" or "the actor is dead"; +// - a plain `!` is dropped, counted by `actors.full_drops(ref)`, and +// reported once on stderr; +// - an ask fails at once with "the mailbox is full". +// +// Here the actor queues work for itself. While its handler runs it handles +// nothing else, so what it sends itself fills its own mailbox. +// +// Run: ae run examples/02-actors/14-bounded-mailbox.ae +// See: docs/language-reference.md#a-bounded-mailbox + +import std.actors + +message Burst { n: int } +message Flood { n: int } +message Line { n: int } +message Count {} + +@mailbox(bound: 3) +actor Ingest { + state handled = 0 + receive { + Burst(n) -> { + i = 1 + while i <= n { + err = try_send(self, Line { n: i }) + if err == "" { + println("line ${i}: sent") + } else { + println("line ${i}: ${err}") + } + i = i + 1 + } + } + Flood(n) -> { + i = 1 + while i <= n { + self ! Line { n: i } // past the bound: dropped and counted + i = i + 1 + } + } + Line(n) -> { handled = handled + 1 } + Count() -> { reply handled } + } +} + +main() { + ingest = spawn(Ingest()) + + ingest ! Burst { n: 5 } + wait_for_idle() + int after_burst = ingest ? Count {} + // print, unlike println, flushes stdout: the lines so far are out + // before the runtime reports the first drop on stderr. + print("handled: ${after_burst}\n") + + ingest ! Flood { n: 5 } + wait_for_idle() + int after_flood = ingest ? Count {} + println("handled: ${after_flood}, dropped by a full mailbox: ${actors.full_drops(ingest)}") + + actors.kill(ingest) + wait_for_idle() + println("after a kill: ${try_send(ingest, Line { n: 0 })}") +} + +// Output: +// line 1: sent +// line 2: sent +// line 3: sent +// line 4: the mailbox is full +// line 5: the mailbox is full +// handled: 3 +// aether: message type 2 to actor 1 dropped: its mailbox is full (bound 3); counted by actors.full_drops (reported once) +// handled: 6, dropped by a full mailbox: 2 +// after a kill: the actor is dead diff --git a/examples/02-actors/15-streams.ae b/examples/02-actors/15-streams.ae new file mode 100644 index 000000000..847cc7b56 --- /dev/null +++ b/examples/02-actors/15-streams.ae @@ -0,0 +1,88 @@ +// A stream: a fast producer held to what its consumer has handled. +// +// A send never blocks, so a producer that outruns its consumer piles +// messages up in memory without bound. std.stream puts a bound back: the +// producer starts with `window` credits, each chunk it sends costs one, +// and the consumer returns one for each chunk it has handled. With none +// left, `stream.offer` returns 0 and the producer stops until the next +// `StreamCredit` comes. So at most `window` chunks are ever in flight, +// and nobody waits. +// +// Run: ae run examples/02-actors/15-streams.ae +// See: std/stream/README.md + +import std.stream + +message Start { s: ptr, total: int } +message More {} +message Report {} + +actor Producer { + state s: ptr = null + state total = 0 + state sent = 0 + state finished = 0 + state peak = 0 + + receive { + Start(s: st, total: t) -> { + s = st + total = t + self ! More {} + } + // Offer lines until the window is full or every line has gone. + More() -> { + going = true + while going && sent < total { + if stream.offer(s, "log line ${sent + 1}") == 1 { + sent = sent + 1 + } else { + going = false // no credit left: wait for the next one + } + } + peak = stream.peak(s) + if sent == total && finished == 0 { + stream.finish(s) + finished = 1 + } + } + StreamCredit(s: st, n) -> { + if stream.credit(st, n) == 0 { + s = null // the consumer has the end: the stream is freed + } else if finished == 0 { + self ! More {} + } + } + Report() -> { reply peak } + } +} + +actor Consumer { + state count = 0 + receive { + StreamChunk(s, data, last) -> { + if last == 0 { + count = count + 1 + } + stream.consumed(s, last) + } + Report() -> { reply count } + } +} + +main() { + p = spawn(Producer()) + c = spawn(Consumer()) + s = stream.connect(p, c, 3) // at most 3 lines in flight + p ! Start { s: s, total: 20 } + wait_for_idle() + + int got = c ? Report {} + int peak = p ? Report {} + println("the consumer handled ${got} lines") + println("the most in flight at once: ${peak}") +} + +// Output: +// the consumer handled 20 lines +// the most in flight at once: 3 diff --git a/examples/02-actors/11-sharded-map.ae b/examples/02-actors/16-sharded-map.ae similarity index 97% rename from examples/02-actors/11-sharded-map.ae rename to examples/02-actors/16-sharded-map.ae index 635d3cea0..69be8bba6 100644 --- a/examples/02-actors/11-sharded-map.ae +++ b/examples/02-actors/16-sharded-map.ae @@ -9,7 +9,7 @@ // meets the same map. docs/design/sharded-actor-map.md has the rationale // and how many shards to use. // -// Run: ae run examples/02-actors/11-sharded-map.ae +// Run: ae run examples/02-actors/16-sharded-map.ae // See: docs/design/sharded-actor-map.md import std.map diff --git a/examples/03-std/10-tcp.ae b/examples/03-std/10-tcp.ae new file mode 100644 index 000000000..92f3d3473 --- /dev/null +++ b/examples/03-std/10-tcp.ae @@ -0,0 +1,77 @@ +// TCP: a server and a client over a socket, in one program. +// +// std.tcp is the layer under std.http: connect, listen, accept, read and +// write, each returning an error string to check. TCP carries a stream of +// bytes, not messages: a read returns what has arrived, which may be part +// of a message or more than one. So a protocol says where a message ends +// (here a newline) and the reader reads until it gets there. The server +// listens on the loopback address only, on a port the system picks. +// +// Run: ae run examples/03-std/10-tcp.ae +// See: std/tcp/README.md + +import std.string +import std.tcp + +message Ask { port: int, text: string } + +// Read one line, without its newline, however the bytes arrive. +read_line(sock: ptr) -> (string, string) { + line = "" + while !string.ends_with(line, "\n") { + part, err = tcp.read(sock, 64) + if err != "" { + return "", err + } + line = string.concat(line, part) + } + return string.substring(line, 0, string.length(line) - 1), "" +} + +// The client: connect, send a line, read the answer. +actor Client { + receive { + Ask(port, text) -> { + sock, err = tcp.connect("127.0.0.1", port) + if err != "" { + println("client: connect failed: ${err}") + } else { + _n, werr = tcp.write(sock, "${text}\n") + if werr == "" { + answer, rerr = read_line(sock) + if rerr == "" { + println("client: the server said '${answer}'") + } + } + tcp.close(sock) + } + } + } +} + +main() { + srv, err = tcp.listen_on("127.0.0.1", 0) + if err != "" { + println("listen failed: ${err}") + return + } + client = spawn(Client()) + client ! Ask { port: tcp.server_port(srv), text: "hello over tcp" } + + // Serve one connection: read a line, answer it in capitals. + sock, aerr = tcp.accept(srv) + if aerr == "" { + line, rerr = read_line(sock) + if rerr == "" { + println("server: got '${line}'") + _n, _werr = tcp.write(sock, "${string.to_upper(line)}\n") + } + tcp.close(sock) + } + wait_for_idle() + tcp.server_close(srv) +} + +// Output: +// server: got 'hello over tcp' +// client: the server said 'HELLO OVER TCP' diff --git a/examples/03-std/11-cryptography.ae b/examples/03-std/11-cryptography.ae new file mode 100644 index 000000000..8509fd0f7 --- /dev/null +++ b/examples/03-std/11-cryptography.ae @@ -0,0 +1,55 @@ +// Cryptography: a hash, a keyed hash, and a signature. +// +// The std.cryptography modules are written in Aether, so they need no +// library on the machine. Three of them cover most of what a program asks: +// - a hash (SHA-256) gives any input a fixed-size fingerprint; +// - an HMAC proves a message came from someone who holds a shared key; +// - a signature (Ed25519) proves it came from the holder of a private key, +// and anyone with the public key can check it. +// They take their input as a `byte[]` slice: `string.bytes(s)` is a +// string's. What they return as a `ptr` is a std.bytes buffer the program +// owns and frees; `wipe_free` zeroes a secret before it lets it go. +// +// Run: ae run examples/03-std/11-cryptography.ae +// See: docs/stdlib-reference.md#cryptography-stdcryptography + +import std.bytes +import std.encoding +import std.string +import std.cryptography.ed25519 +import std.cryptography.hmac +import std.cryptography.sha2 + +main() { + // A hash: the same input always gives the same 32 bytes. + println("sha256(abc) = ${sha2.sha256_hex(string.bytes("abc"))}") + + // An HMAC: only a holder of the key can make the same tag. + msg = "pay 10 to alice" + tag = hmac.hmac_sha256_hex(string.bytes("shared key"), string.bytes(msg)) + again = hmac.hmac_sha256_hex(string.bytes("shared key"), string.bytes(msg)) + other = hmac.hmac_sha256_hex(string.bytes("wrong key"), string.bytes(msg)) + println("hmac matches with the key: ${tag == again}, with another key: ${tag == other}") + + // A signature. The 32-byte private seed comes from a hash here so the + // output is the same every run; a real one comes from random bytes + // (std.cryptography's random_bytes) and is kept secret. + seed = sha2.sha256_bytes(string.bytes("an example seed, never a real one")) + public = ed25519.publickey(seed) + sig = ed25519.sign(seed, string.bytes(msg)) + println("public key: ${encoding.hex_encode(bytes.view(public))}") + println("signature verifies: ${ed25519.verify(public, string.bytes(msg), bytes.view(sig)) == 1}") + forged = "pay 99 to alice" + println("on a changed message: ${ed25519.verify(public, string.bytes(forged), bytes.view(sig)) == 1}") + + bytes.wipe_free(seed) + bytes.free(public) + bytes.free(sig) +} + +// Output: +// sha256(abc) = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad +// hmac matches with the key: true, with another key: false +// public key: d1e633b86e5acea0fb20c459575bb672fed1e2c97af24561911bbeedd4ec96c3 +// signature verifies: true +// on a changed message: false diff --git a/examples/04-interop/07-limits/host.c b/examples/04-interop/07-limits/host.c new file mode 100644 index 000000000..c72846f78 --- /dev/null +++ b/examples/04-interop/07-limits/host.c @@ -0,0 +1,75 @@ +/* The C program of examples/04-interop/07-limits: it runs script.ae's + * exports under a deadline, a cancel from another thread and an execution + * account, as run.sh links them. */ +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +int32_t aether_spin(void); +int32_t aether_grow(int32_t n); + +static const char* outcome_name(int outcome) { + switch (outcome) { + case AETHER_OK: return "ok"; + case AETHER_PANICKED: return "panicked"; + case AETHER_DEADLINE: return "deadline"; + case AETHER_CANCELLED: return "cancelled"; + case AETHER_MEMORY_CAP: return "memory cap"; + case AETHER_OUT_OF_MEMORY: return "out of memory"; + default: return "another failure"; + } +} + +static void report(const char* what, int32_t result) { + printf("%s: returned %d, %s\n", what, (int)result, outcome_name(aether_last_outcome())); +} + +static void sleep_ms(int ms) { +#ifdef _WIN32 + Sleep((DWORD)ms); +#else + struct timespec t = { 0, (long)ms * 1000000L }; + nanosleep(&t, NULL); +#endif +} + +/* Cancels the call the token names, once it has run a little. */ +static void* cancel_soon(void* arg) { + sleep_ms(20); + aether_exec_cancel(*(AetherExecToken*)arg); + return NULL; +} + +int main(void) { + /* A deadline for every call this thread makes, until it is set to 0. */ + aether_set_call_deadline(50); + report("spin under a 50 ms deadline", aether_spin()); + aether_set_call_deadline(0); + + /* A cancel: the token names the next call this thread makes. */ + AetherExecToken token = aether_exec_token(); + pthread_t canceller; + pthread_create(&canceller, NULL, cancel_soon, &token); + report("spin cancelled from another thread", aether_spin()); + pthread_join(canceller, NULL); + + /* A budget: 64 KiB of live memory for everything the script holds. */ + AetherExecLimits limits = { 64 * 1024, 0, AETHER_DEADLINE_ELAPSED, NULL }; + AetherExecAccount* script = aether_exec_account_new(&limits); + AetherExecAccount* host_own = NULL; + aether_exec_use(script, &host_own); /* this thread's calls charge `script` */ + report("grow(100) under a 64 KiB budget", aether_grow(100)); + report("grow(100000) under the same budget", aether_grow(100000)); + int tripped = aether_exec_account_tripped(script) & AETHER_ACCOUNT_TRIPPED_MEMORY; + printf("the budget was exceeded: %s\n", tripped ? "yes" : "no"); + report("grow(10) after that", aether_grow(10)); + aether_exec_use(host_own, NULL); /* back to the host's own */ + aether_exec_account_free(script); + return 0; +} diff --git a/examples/04-interop/07-limits/run.sh b/examples/04-interop/07-limits/run.sh new file mode 100644 index 000000000..79c49d57f --- /dev/null +++ b/examples/04-interop/07-limits/run.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Build examples/04-interop/07-limits: script.ae as an object file, host.c +# linked against it and the Aether runtime, then run the program. +# Run from the repository root. AE and CC name the tools (default: ae, cc). +set -e +AE="${AE:-ae}" +CC="${CC:-cc}" +here="$(dirname "$0")" +out="build/examples/limits" +mkdir -p "$out" +"$AE" build --emit=obj "$here/script.ae" -o "$out/script.o" >/dev/null +# shellcheck disable=SC2046 +"$CC" $("$AE" cflags --cflags) "$here/host.c" "$out/script.o" $("$AE" cflags --libs) -o "$out/host" +"$out/host" diff --git a/examples/04-interop/07-limits/script.ae b/examples/04-interop/07-limits/script.ae new file mode 100644 index 000000000..1e77feb3b --- /dev/null +++ b/examples/04-interop/07-limits/script.ae @@ -0,0 +1,51 @@ +// A host's limits on the Aether code it runs: a deadline, a cancel and a +// budget. +// +// A program that embeds Aether to run code it does not control (a game's +// scripts, an editor's plugins) bounds what that code may take. Every +// exported function runs in an execution frame, so however a call ends, +// the host gets control back with an outcome (`aether_last_outcome()`): +// - a deadline (`aether_set_call_deadline(ms)`) ends a call that runs too +// long, at its next loop head, with AETHER_DEADLINE; +// - another thread ends a running call with `aether_exec_cancel(token)`, +// where the token is `aether_exec_token()` of the calling thread, with +// AETHER_CANCELLED; +// - an execution account (`aether_exec_account_new`) gives a script a +// memory budget (and a deadline) of its own, across all its calls: an +// allocation past it ends the call with AETHER_MEMORY_CAP. +// The call returns its result's zero value, its defers run and what it +// owned is freed. host.c is the program and run.sh builds and runs it; +// `ae cflags` names the include path and the runtime to link. +// +// Run: sh examples/04-interop/07-limits/run.sh +// See: docs/emit-lib.md#per-call-resource-caps + +import std.string + +// Counts for ever: only a deadline or a cancel ends it, and the call then +// returns 0, an int's zero value. +spin() -> int { + n = 0 + while true { + n = n + 1 + } +} + +// The length of a string of n * 16 bytes, built a piece at a time. +grow(n: int) -> int { + s = "" + i = 0 + while i < n { + s = string.concat(s, "0123456789abcdef") + i = i + 1 + } + return string.length(s) +} + +// Output: +// spin under a 50 ms deadline: returned 0, deadline +// spin cancelled from another thread: returned 0, cancelled +// grow(100) under a 64 KiB budget: returned 1600, ok +// grow(100000) under the same budget: returned 0, memory cap +// the budget was exceeded: yes +// grow(10) after that: returned 160, ok diff --git a/examples/README.md b/examples/README.md index 77e026dff..9f8446de0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,9 +13,13 @@ Run them from the repository root: ae run examples/01-language/01-hello.ae ``` -With a toolchain built from this tree, that is `./build/ae run ...`. The -[language reference](../docs/language-reference.md) is the full story -behind each step; every row below links the part it builds on. +With a toolchain built from this tree, that is `./build/ae run ...`. +[The tutorial](../docs/tutorial.md) walks this path in prose, a chapter +per part below, with the mistakes newcomers make and exercises; the +[glossary](../docs/glossary.md) defines its words. The [language +reference](../docs/language-reference.md) is the full story behind each +step; every row below links the part it builds on. [Getting +started](../docs/getting-started.md) installs the toolchain. ## 1. The language @@ -30,7 +34,7 @@ behind each step; every row below links the part it builds on. | [07-enums-and-sums.ae](01-language/07-enums-and-sums.ae) | `enum`, sum types, exhaustive `match` | [Sum types](../docs/language-reference.md#sum--variant-types) | | [08-match.ae](01-language/08-match.ae) | literals, ranges, strings, list and struct patterns | [Match expressions](../docs/language-reference.md#match-expressions) | | [09-optionals.ae](01-language/09-optionals.ae) | `T?`, `none`, `??`, `?.`, narrowing | [Optionals](../docs/language-reference.md#optionals) | -| [10-errors.ae](01-language/10-errors.ae) | `(value, err)`, `T!`, `or`, `!`, `panic` and `try` | [Result types](../docs/language-reference.md#the-t-result-type-and-or--sugar) | +| [10-errors.ae](01-language/10-errors.ae) | `(value, err)`, `T!`, `or`, `!`, `panic` and `try` | [Result types](../docs/language-reference.md#the-t-result-type-and-or---sugar) | | [11-arrays-and-slices.ae](01-language/11-arrays-and-slices.ae) | fixed arrays, slices, `.len`, checked indexes, `make` | [Slices](../docs/language-reference.md#slices-t) | | [12-collections.ae](01-language/12-collections.ae) | std.list, std.map, std.set, sorting | [Handles](../docs/memory-management.md#handles-and-defer) | | [13-closures.ae](01-language/13-closures.ae) | closures, captured variables, `fn` parameters, trailing blocks | [Closures](../docs/closures-and-builder-dsl.md) | @@ -53,8 +57,12 @@ behind each step; every row below links the part it builds on. | [08-state-machine.ae](02-actors/08-state-machine.ae) | an enum as state, a transition table | [Enums](../docs/language-reference.md#enums) | | [09-timeouts.ae](02-actors/09-timeouts.ae) | `receive ... after`, a watchdog | [Actor timeouts](../docs/actor-concurrency.md#actor-timeouts) | | [10-crash-isolation.ae](02-actors/10-crash-isolation.ae) | a panic stays in its actor, and the others go on | [Panics](../docs/actor-concurrency.md#panic-and-stack-traces) | -| [11-sharded-map.ae](02-actors/11-sharded-map.ae) | keys split over owner actors instead of a lock | [Sharded actor map](../docs/design/sharded-actor-map.md) | -| [12-supervisor.ae](02-actors/12-supervisor.ae) | a crashed actor restarted under the same ref, and a supervisor that gives up | [Supervisors](../docs/actor-concurrency.md#supervisors) | +| [11-ending.ae](02-actors/11-ending.ae) | `actors.stop`, `actors.kill`, a panic; the reason a monitor's `Down` carries | [The end of an actor](../docs/actor-concurrency.md#the-end-of-an-actor) | +| [12-links.ae](02-actors/12-links.ae) | `actors.link`, `actors.trap_exits`: a failure climbs the links | [Links](../docs/actor-concurrency.md#links) | +| [13-supervisor.ae](02-actors/13-supervisor.ae) | a crashed actor restarted under the same ref, and a supervisor that gives up | [Supervisors](../docs/actor-concurrency.md#supervisors) | +| [14-bounded-mailbox.ae](02-actors/14-bounded-mailbox.ae) | `@mailbox(bound: N)`, `try_send`, what a full mailbox does to `!` | [A bounded mailbox](../docs/language-reference.md#a-bounded-mailbox) | +| [15-streams.ae](02-actors/15-streams.ae) | std.stream: a producer held to a window of credits | [std.stream](../std/stream/README.md) | +| [16-sharded-map.ae](02-actors/16-sharded-map.ae) | keys split over owner actors instead of a lock | [Sharded actor map](../docs/design/sharded-actor-map.md) | ## 3. The standard library @@ -69,6 +77,8 @@ behind each step; every row below links the part it builds on. | [07-reverse-proxy.ae](03-std/07-reverse-proxy.ae) | forwarding to an upstream with std.http.proxy | [Reverse proxy](../docs/http-reverse-proxy.md) | | [08-proxy-pool.ae](03-std/08-proxy-pool.ae) | a weighted pool configured as code | [Reverse proxy](../docs/http-reverse-proxy.md) | | [09-logging.ae](03-std/09-logging.ae) | std.log levels, a log file, the threshold | [stdlib reference](../docs/stdlib-reference.md) | +| [10-tcp.ae](03-std/10-tcp.ae) | std.tcp: a server and a client, reading a stream up to where a message ends | [std.tcp](../std/tcp/README.md) | +| [11-cryptography.ae](03-std/11-cryptography.ae) | SHA-256, an HMAC, an Ed25519 signature and its check | [Cryptography](../docs/stdlib-reference.md#cryptography-stdcryptography) | ## 4. C and other languages @@ -88,6 +98,7 @@ builds and runs them. | [05-hosts/go.ae](04-interop/05-hosts/go.ae) | a Go program as a sandboxed child process | [Host modules](../docs/containment-sandbox.md#language-host-modules) | | [05-hosts/tinygo.ae](04-interop/05-hosts/tinygo.ae) | a Go c-shared library loaded and called in-process | [contrib/host/tinygo](../contrib/host/tinygo/README.md) | | [06-java-host/aether/manifest.ae](04-interop/06-java-host/aether/manifest.ae) | a namespace with a generated Java SDK, called from Java | [Embedded namespaces](../docs/embedded-namespaces-and-host-bindings.md) | +| [07-limits/script.ae](04-interop/07-limits/script.ae) | a host's limits on the code it runs: a deadline, a cancel, a memory budget | [Per-call resource caps](../docs/emit-lib.md#per-call-resource-caps) | ## 5. Containing code diff --git a/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md b/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md new file mode 100644 index 000000000..2e3ed9867 --- /dev/null +++ b/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md @@ -0,0 +1,11 @@ +- **The examples path teaches what batch 9 added (#3476).** Six new + examples, each with its output checked by `make check-examples`: + ending an actor (`actors.stop`, `actors.kill`, a panic, and the reason + a monitor's `Down` carries), links and trapping exits, a bounded mailbox + with `try_send`, a std.stream producer held to a window of credits, + std.tcp (a server and a client, reading a stream up to where a message + ends), the pure-Aether cryptography (SHA-256, an HMAC, an Ed25519 + signature), and a C host's limits on the code it runs (a deadline, a + cancel from another thread, an execution account's memory budget). + Links now come before the supervisor built on them: `12-supervisor.ae` + is `13-supervisor.ae`, and `11-sharded-map.ae` is `16-sharded-map.ae`. From 43c5cda087f8095d181dc86c98c0d42c13a75122 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 15:58:27 -0300 Subject: [PATCH 03/13] docs: one way to learn Aether (#3475) 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. --- README.md | 13 +- docs/getting-started.md | 649 ++----------- docs/package-fetching.md | 64 +- docs/tutorial.md | 907 +++++++++++------- docs/tutorials/01-hello-aether.md | 286 ------ .../02-functions-and-control-flow.md | 637 ------------ docs/tutorials/03-actor-concurrency.md | 616 ------------ ...T130100Z-changed-3475-one-learning-path.md | 10 + tests/regression/test_actor_pair_fifo.ae | 4 +- 9 files changed, 744 insertions(+), 2442 deletions(-) delete mode 100644 docs/tutorials/01-hello-aether.md delete mode 100644 docs/tutorials/02-functions-and-control-flow.md delete mode 100644 docs/tutorials/03-actor-concurrency.md create mode 100644 new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md diff --git a/README.md b/README.md index 43c4987cc..898ec0bbb 100644 --- a/README.md +++ b/README.md @@ -389,10 +389,17 @@ Same file is config, validation, conditional logic, and the entry point. No seco ## Documentation -- [Getting Started Guide](docs/getting-started.md) - Installation and first steps -- [Bootstrapping from source (HEAD)](docs/bootstrap-from-source.md) - Build + install the toolchain and contrib straight from this repo (for consumers tracking HEAD or pre-package); humans and LLMs -- [Language Tutorial](docs/tutorial.md) - Learn Aether syntax and concepts +**Learn Aether**, in this order: + +- [Getting Started](docs/getting-started.md) - Install, a first program, a project, the tools +- [The path through the examples](examples/README.md) - One idea per file, from the first program to actors, std, C, the sandbox and whole programs; every output checked in CI +- [The tutorial](docs/tutorial.md) - The path told in prose, a chapter per part, with common mistakes and exercises +- [Glossary](docs/glossary.md) - The words the others use, defined - [Language Reference](docs/language-reference.md) - Complete language specification + +**Reference and guides:** + +- [Bootstrapping from source (HEAD)](docs/bootstrap-from-source.md) - Build + install the toolchain and contrib straight from this repo (for consumers tracking HEAD or pre-package); humans and LLMs - [Standard Library Reference](docs/stdlib-reference.md) - Full stdlib surface - [Testing](docs/testing.md) - `std.spec` BDD framework (describe/it, assertions, fluent chain) + `ae test` discovery and `--format=tap|aeocha-v1` reporting - [HTTP Server](docs/http-server.md) - TLS, HTTP/2, middleware, health probes, metrics, graceful shutdown diff --git a/docs/getting-started.md b/docs/getting-started.md index c23aeb33b..5e3b75ca0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,9 @@ # Getting Started with Aether -This guide covers installation and basic usage of the Aether programming language. +This page installs Aether, runs a first program, and starts a project. The +language itself is taught by [the path through the examples](../examples/README.md), +with [the tutorial](tutorial.md) beside it; [Learn Aether](#learn-aether) at +the end says how they fit together. ## Installation @@ -121,465 +124,47 @@ After installation, open any `.ae` file and VS Code will automatically apply syn ## Your First Aether Program -**Option A: Create a project (recommended)** - -```bash -ae init myproject -cd myproject -ae run -``` - -This creates a project with `aether.toml`, `src/main.ae`, and `tests/`. - -**Option B: Run a single file** - Create `hello.ae`: ```aether main() { - println("Hello, Aether!") + name = "Aether" + println("Hello, ${name}!") } ``` +Run it: + ```bash ae run hello.ae ``` -**Build an executable:** +`ae run` compiles the file to C, builds it and runs it, keeping the build +in a cache so the next run of an unchanged file starts at once. The other +everyday commands: ```bash -ae build hello.ae -o hello +ae build hello.ae -o hello # an executable ./hello -``` - -**Build for WebAssembly:** - -```bash -ae build --target wasm hello.ae +ae check hello.ae # type-check only, no build +ae fmt hello.ae # format in place (--check to only check) +ae build --target wasm hello.ae # WebAssembly; needs Emscripten (emcc) on PATH node hello.js ``` -Requires [Emscripten](https://emscripten.org/) (`emcc` on PATH). - -**Type-check without compiling:** +## A Project ```bash -ae check hello.ae -``` - -**Add a package (GitHub, GitLab, Bitbucket, Codeberg, or a self-hosted forge):** - -```bash -ae add github.com/user/repo # the default branch -ae add github.com/user/repo@v1.0.0 # specific version -ae add gitlab.com/user/repo # GitLab -ae add codeberg.org/user/repo # Codeberg -ae add github.com/user/repo@v1.1.0 --update # move a pinned dependency -ae add github.com/user/repo@v1.0.0 --source # the source, even with a release artifact -``` - -`ae add` takes the source archive the forge serves for the tag (or for the -default branch) and needs no git. It pins the hash of the tree in -`aether.toml`'s `[dependency-hashes]`: a later `ae add` must get the same -tree, a build refuses an installed package that is not it, and `--update` -is how a pin moves. Without `@version` it installs the version -`aether.toml` names, so on a fresh checkout `ae add ` installs the -pinned dependency. [package-fetching.md](package-fetching.md) has the -details, mirrors included. - -With `@version`, `ae add` prefers a **published release artifact** when the -package ships one for your platform, and takes the source archive otherwise. -The asset is expected at the same layout Aether's own releases use: - -``` -https:///releases/download//---.tar.gz -``` - -(`.zip` is tried too; `-` is `linux-x86_64`, `macos-arm64`, and -so on.) A sibling `.sha256` is verified when present: a -**mismatch is fatal and installs nothing**, and there is no silent -fall-back to the source, which would defeat the check. An artifact with no -published checksum installs with a warning. - -Prebuilt `--emit=lib` artifacts are directly importable: the binary-import -prepass reads the artifact's catalog and synthesizes the interface, so a -released library needs no toolchain to consume. Set -`AE_RELEASE_BASE_URL` to point at an internal mirror serving the same -layout, and the source archives at `/archive/.tar.gz` -([package-fetching.md](package-fetching.md)). - -For a **binary package**, publish `aether.toml` with a `[package]` section -containing `binary = ""` and `modules = "."`, plus -`--.` and its -`.sha256` sidecar. Unlike the archive path, a binary package requires the -checksum: a missing or mismatched checksum is fatal. If the declared binary -is missing for the selected target, `ae add` fails without trying another -libc or a source archive. - -Publish Linux binary packages under distinct glibc and musl identities: - -| Build target | Release triple | -| --- | --- | -| `x86_64-linux` (glibc) | `linux-x86_64` | -| `aarch64-linux` (glibc) | `linux-arm64` | -| `x86_64-linux-musl` | `linux-x86_64-musl` | -| `aarch64-linux-musl` | `linux-arm64-musl` | - -Automatic selection uses the CPU and libc that `ae` was built for, retaining -the existing unsuffixed glibc names. To fetch a foreign binary, give its -**release triple** explicitly (rather than the compiler's build-target spelling): - -```bash -ae add github.com/user/repo@v1.0.0 --target linux-arm64-musl -``` - -The explicit target is preserved in the asset name; it does not fall back to -the host's libc. The package cache holds one target per package. Use a separate -`HOME` per target when assembling a multi-target release. `ae add` refuses to -reuse an installed binary for a different target and preserves its bytes. -Older binary caches without a target record must be removed and fetched again -so the selected asset and its checksum are verified. - -For example, a stem of `libfoo` yields -`libfoo-v1.0.0-linux-arm64-musl.so` and -`libfoo-v1.0.0-linux-arm64-musl.so.sha256`. - -## Error Handling - -Functions that can fail return `(value, error)` tuples. Check the error first, handle it, then continue, no `else` needed: - -```aether -safe_divide(a: int, b: int) -> { - if b == 0 { - return 0, "division by zero" - } - return a / b, "" -} - -main() { - result, err = safe_divide(10, 3) - if err != "" { - println("Error: ${err}") - exit(1) - } - println("Result: ${result}") - - // Discard error with _ - val, _ = safe_divide(42, 7) - println(val) -} -``` - -## Function Contracts (`requires` / `ensures`) - -Aether supports Eiffel-style runtime contracts, pre/postconditions you attach directly to a function declaration. They document intent at the boundary, fire as `aether_panic` on violation (which prints the failed predicate by name), and elide entirely when the predicate is provably constant-true at compile time. - -```aether,fragment -add(a: int, b: int) -> int - requires a >= 0 - requires b >= 0 - ensures result >= a - ensures result >= b -{ - return a + b -} - -safe_div(a: int, b: int) -> int - requires b != 0 -{ - return a / b -} -``` - -- `requires ` runs at function entry, with parameters in scope. -- `ensures ` runs before each `return`, with `result` bound to the value about to be returned. -- Multiple clauses, freely interleaved, each is checked independently so the panic message names the specific failed predicate. -- `aetherc --no-contracts` (analog of `-DNDEBUG`) drops every check at codegen for release builds. - -See [Function contracts](language-reference.md#function-contracts-requires--ensures) for the full semantics, the const-fold elision rules, and v1 limitations. - -## Interactive REPL - -Experiment with Aether interactively: - -```bash -ae repl -``` - -``` - ┌───────────────────────────┐ - │ Aether REPL │ - │ :help for commands │ - └───────────────────────────┘ - -ae> x = 5 - -ae> y = 3 - -ae> println(x + y) - -8 -ae> :show - x = 5 - y = 3 -ae> :quit -``` - -Assignments and constants persist across evaluations. Multi-line blocks (if/while/for) auto-continue until braces close. Use `:reset` to clear the session, `:show` to see accumulated code. - -## Actor-Based Programming - -Aether is built around the actor model. Here is a simple counter example: - -```aether -message Ping {} - -actor Counter { - state count = 0 - - receive { - Ping() -> { - count = count + 1 - } - } -} - -main() { - c = spawn(Counter()) - c ! Ping {} - c ! Ping {} - c ! Ping {} - - // Wait for all messages to be processed - wait_for_idle() - - println("Count: ${c.count}") -} -``` - -Actors are lightweight concurrent entities that communicate through asynchronous messages. Each actor has private state and a mailbox for incoming messages. Messages are defined with the `message` keyword and sent with the `!` operator. The runtime distributes actors across available CPU cores automatically. - -Use `wait_for_idle()` to block until all actors have finished processing their messages. This is essential when you need to read actor state or coordinate completion. - -## Module System - -Import modules using the `import` statement: - -```aether -import std.map - -main() { - mymap = map.new() - defer map.free(mymap) - - map.put(mymap, "greeting", "hello") - - println("Map created") -} -``` - -Functions are called using **namespace-style syntax**: `namespace.function()`. -Stdlib functions that can fail return `(value, err)` tuples, -check `err` first, then use `value`: - -```aether,fragment -body, err = http.get("http://example.com") -if err != "" { println("failed: ${err}"); return } -println(body) -``` - -| Import | Namespace | Example | -|--------|-----------|---------| -| `import std.string` | `string` | `string.length(s)`, `n, err = string.to_int("42")` | -| `import std.file` | `file` | `content, err = file.read("f")`, `file.exists("f")` | -| `import std.dir` | `dir` | `err = dir.create("d")`, `dir.exists("d")` | -| `import std.path` | `path` | `path.join("a", "b")`, `path.basename("a/b")` | -| `import std.list` | `list` | `list.new()`, `err = list.add(l, item)` | -| `import std.map` | `map` | `map.new()`, `err = map.put(m, k, v)` | -| `import std.json` | `json` | `v, err = json.parse(str)`, `json.obj()` | -| `import std.math` | `math` | `math.abs_int(x)`, `math.sqrt(x)` | -| `import std.http` | `http` | `body, err = http.get(url)`, `http.server_create(port)` | -| `import std.tcp` | `tcp` | `sock, err = tcp.connect(host, port)`, `tcp.write(sock, data)` | -| `import std.log` | `log` | `err = log.init("app.log", 0)`, `log.write(0, msg)` | -| `import std.io` | `io` | `content, err = io.read_file("f")`, `io.getenv("HOME")` | -| `import std.os` | `os` | `out, err = os.exec("ls")`, `os.system("cmd")` | - -Raw externs are preserved under a `_raw` suffix (e.g. `http.get_raw`) for -advanced callers who need direct access to the underlying ptr or status code. - -### Creating Your Own Modules - -You can write reusable modules in pure Aether, no C required. Place your module in `lib//module.ae`: - -**lib/mymath/module.ae:** -```aether,fragment -export const PI = 3 - -export double_it(x) { - return multiply(x, 2) -} - -export add(a, b) { - return a + b -} - -// Private helper, not accessible from outside -multiply(a, b) { - return a * b -} -``` - -**src/main.ae:** -```aether,fails -import mymath - -main() { - println(mymath.double_it(5)) // 10 - println(mymath.add(3, 4)) // 7 - println(mymath.PI) // 3 - // mymath.multiply(2, 3) // Error: not exported -} -``` - -Use `export` to control which functions and constants are part of your module's public API. Non-exported symbols are private, they can be used internally by exported functions but are not accessible to importers. If a module has no `export` declarations, all symbols are public (backwards compatible). - -Modules support functions, constants, intra-module calls (functions calling other functions in the same module), and export visibility. See [Module System Design](module-system-design.md#pure-aether-modules) for full details. - -## Standard Library - -Aether includes a standard library with the following modules: - -| Module | Description | -|--------|-------------| -| `std.string` | String operations | -| `std.file` | File operations | -| `std.dir` | Directory operations | -| `std.path` | Path utilities | -| `std.list` | Dynamic array (ArrayList) | -| `std.map` | Hash map (HashMap) | -| `std.json` | JSON parsing and creation | -| `std.http` | HTTP client and server | -| `std.tcp` | TCP sockets | -| `std.log` | Structured logging | -| `std.math` | Math functions | -| `std.io` | Console I/O, environment variables | -| `std.os` | Shell execution, command output capture | - -See the [Standard Library Reference](stdlib-reference.md) for every module and its API. - -> When you read a file or get data from stdlib functions, the result is a -> regular string: you can `print()` it, use it in `"${interpolation}"`, or pass it in messages -> directly. No conversion needed. - -### Error Handling and Memory - -Stdlib functions that can fail return a `(value, err)` tuple. -Check the error string first, then use the value. A string a stdlib -function returns is a counted value that the local holding it owns, so -there is nothing to free: - -```aether -import std.io - -main() { - // io.getenv is infallible; returns null if unset - home = io.getenv("HOME") - if home != null { - println(home) - } - - // io.read_file returns (value, err) - content, err = io.read_file("data.txt") - if err != "" { - println("cannot read: ${err}") - return - } - println(content) -} -``` - -A string needs no cleanup: the local, field or element holding it releases -it when it is overwritten or goes out of scope. `free(s)` or `release(s)` on -a string releases it early and leaves the place empty. - -A handle is different: a list, a map or a C buffer is a `ptr` the program -frees itself, through its module (`list.free`, `map.free`), or with the -`free()` builtin for memory from `make` or `malloc`. `defer` keeps the free -next to the allocation. See [Memory Management](memory-management.md). - -## Pattern Matching - -Functions can be defined as several clauses, each matching a different shape of -argument, with guards to narrow a clause further. The first clause that matches -runs. - -### Function Pattern Matching - -Define functions with multiple clauses that match on argument values: - -```aether,fragment -// Match on literal values -factorial(0) -> 1 -factorial(n) when n > 0 -> n * factorial(n - 1) - -// Fibonacci with multiple base cases -fib(0) -> 0 -fib(1) -> 1 -fib(n) when n > 1 -> fib(n - 1) + fib(n - 2) - -// Guards for conditional matching -classify(x) when x < 0 -> "negative" -classify(x) when x == 0 -> "zero" -classify(x) when x > 0 -> "positive" - -// Multi-parameter pattern matching -gcd(a, 0) -> a -gcd(a, b) when b > 0 -> gcd(b, a - (a / b) * b) -``` - -This style replaces verbose if/else chains with declarative, readable code. - -### Match Statements - -Use `match` for value dispatch: - -```aether,fragment -match (value) { - 0 -> { println("Zero") } - 1 -> { println("One") } - _ -> { println("Other") } -} -``` - -### List Patterns - -A slice (`int[]`) matches list patterns: `[]` for empty, `[x]` for one -element, and `[h | t]` for a head and the rest, itself a slice. An array -passes to a slice parameter with its length: - -```aether,run -describe(nums: int[]) -> string { - return match nums { - [] -> "empty" - [x] -> "one: ${x}" - [h | t] -> "head ${h}, ${t.len} more" - } -} - -main() { - println(describe([1, 2, 3])) - println(describe([])) -} -``` - -```output -head 1, 2 more -empty +ae init myproject +cd myproject +ae run +ae test ``` -## Project Configuration - -Projects use `aether.toml` for configuration. Created automatically by `ae init`: +`ae init` writes `aether.toml`, `src/main.ae` and `tests/`. `ae run` and +`ae build` in the directory build the program `aether.toml` names, and +`ae test` runs the tests under `tests/` ([Testing](testing.md)). The +configuration: ```toml [package] @@ -599,97 +184,64 @@ target = "native" # link_flags = "-lsqlite3 -lcurl" # Link external C libraries ``` -### Linking C Libraries +`link_flags` links C libraries a program calls through `extern` +([C interoperability](c-interop.md)). A project's own modules live in +`lib//module.ae`; [the modules example](../examples/01-language/14-modules/main.ae) +shows one, and [`--lib`](../examples/06-tooling/04-lib-path/main.ae) adds +more directories to search. -To link external C libraries, add `link_flags` to the `[build]` section: - -```toml -[build] -link_flags = "-lsqlite3 -lcurl -lssl" -``` - -This allows Aether programs to use libraries like SQLite, libcurl, OpenSSL, etc. - -### Command-Line Arguments - -Read command-line arguments through `std.os`: - -```aether -import std.os - -main() { - count = os.args_count() - for (i = 0; i < count; i = i + 1) { - println(os.args_get(i)) - } -} -``` - -To pass arguments, build and run the binary directly: +### Packages ```bash -ae build myprogram.ae -o myprogram -./myprogram arg1 arg2 +ae add github.com/user/repo # the default branch +ae add github.com/user/repo@v1.0.0 # a version +ae add github.com/user/repo@v1.1.0 --update # move a pinned dependency ``` -### Environment Variables - -Read configuration from environment variables using `std.os`: - -```aether -import std.os - -main() { - home = os.getenv("HOME") - if home != null { - println("Home directory: ${home}") - } -} -``` +GitHub, GitLab, Bitbucket, Codeberg and self-hosted forges work the same +way. `ae add` needs no git: it takes the archive the forge serves, pins the +hash of its tree in `aether.toml`, and refuses a later fetch that is not +that tree. With a version, it takes the package's release artifact for your +platform when the package publishes one. [Package fetching](package-fetching.md) +has the rest: mirrors, release artifacts, and publishing binary packages. -The builtin `getenv()` also works without an import for quick scripts; its string is released with the local that holds it. +## Tools for When You Are Stuck -## When something doesn't compile +**`ae help `** turns the compiler's terse findings into +suggestions: a misspelt name, a missing import, a YAML-shaped line inside a +builder block. `--fix` applies the safe rewrites after showing the diff, +and `--json` is for CI. It makes no network call and runs nothing +([Config-IS-Code Diagnostics](cic-help.md), +[an example](../examples/06-tooling/05-ae-help/broken.ae)). -Aether's typer is precise, `error[E0301]: Undefined function 'super_token'` is the right output for a build pipeline but not always for an operator authoring a config script. `ae help ` translates the typer's terse output into actionable, on-machine suggestions: +**`ae repl`** evaluates as you type. Assignments and constants persist, +multi-line blocks continue until their braces close, `:show` prints the +session, `:reset` clears it and `:quit` leaves. -```bash -ae help my_script.ae # Human-readable findings -ae help my_script.ae --json # Machine-readable (CI integration) -ae help my_script.ae --fix # Apply safe rewrites after diff confirmation ``` - -Heuristics cover Levenshtein-matched name suggestions, YAML/HCL-shape detection inside closure-DSL blocks (`port: 9990` → `port(9990)`), missing-import detection against the stdlib catalog, type-mismatch English ("Drop the quotes"), and library-author-shipped `*.help.md` hints. Hard privacy contract: no network calls, no file reads outside the script + its imports + co-located hints, no execution of the script. See [Config-IS-Code Diagnostics](cic-help.md) and [`examples/06-tooling/05-ae-help/`](../examples/06-tooling/05-ae-help/broken.ae) for a worked example. - -## Layering modules across multiple directories - -PATH-style `--lib` chains let you layer project-local module overrides on top of vendored or shared roots: - -```bash -# Try a local patch in lib/foo/ first, fall back to vendor/foo/: -ae run main.ae --lib lib --lib vendor - -# Same as a separator-string (':' POSIX, ';' Windows): -ae run main.ae --lib "lib:vendor" - -# Or via env var: -AETHER_LIB_DIR="lib:vendor" ae run main.ae - -# See what the toolchain will actually search, in order: -ae lib-path --lib "lib:vendor" +ae> x = 5 +ae> println(x * 2) +10 +ae> :quit ``` -Left-most entry wins on a name collision; each `import` walks the chain independently. See [`examples/06-tooling/04-lib-path/`](../examples/06-tooling/04-lib-path/main.ae) for a runnable demo and [Module System](module-system-design.md#lib-search-path) for the design. +## Learn Aether -## Next Steps +Three documents, each with one job: -- Read the [Tutorial](tutorial.md) for a guided introduction -- Learn about [C Interoperability](c-interop.md) to use C libraries -- See [C Embedding Guide](c-embedding.md) to embed Aether actors in C applications -- Explore [Standard Library Documentation](stdlib-reference.md) -- See [Architecture](architecture.md) for compiler and runtime internals -- See [Runtime Optimizations](runtime-optimizations.md) for performance details -- Use `ae upgrade` to get the latest release, or `ae version list` to see and switch between releases +1. **[The path](../examples/README.md)**: the examples in order, from the + first program to actors, the standard library, C, the sandbox, the + tools and whole programs. Each is one file that teaches one idea, says + how to run it, and ends with the output it prints; CI checks that it + still does. +2. **[The tutorial](tutorial.md)**: the path told as a story. A chapter per + part, saying what each step shows and why it comes there, the mistakes + newcomers make, and exercises with their answers. +3. **[The language reference](language-reference.md)**: everything, to look + up. The [glossary](glossary.md) defines the words the others use. + +Start with the first chapter of the tutorial and its examples, run each +one, and change it. ## Version Management @@ -759,36 +311,7 @@ the old size, because it counts every reflinked file at full size even though the space really has come back. `df` shows the truth. After a *hardlink* dedupe `du` drops, because it counts a shared inode once. -## Troubleshooting - -### Build Failures - -**"Aether compiler not found" (Windows)** -- Make sure you **restarted your terminal** after adding `C:\aether\bin` to PATH -- Verify: `where ae` should show `C:\aether\bin\ae.exe` -- Verify: `where aetherc` should show `C:\aether\bin\aetherc.exe` -- If both exist but it still fails, set `AETHER_HOME`: `set AETHER_HOME=C:\aether` - -**"gcc: command not found" (Windows)** -- This should not happen with the pre-built binary, `ae` auto-downloads GCC (~80 MB) on first run -- If you built from source via MSYS2, open the "MSYS2 MinGW 64-bit" shell, not plain PowerShell -- Verify: `gcc --version` should show MinGW-w64 GCC - -**"pthread.h: No such file or directory"** -- Linux: `sudo apt-get install libpthread-stubs0-dev` -- Windows: The Aether runtime uses Win32 threads natively, no pthread library needed - -**Test failures** -- Run `make test` for unit tests, `make test-ae` for integration tests -- Check for port conflicts if network tests fail (port 8080) - -### Quick tips when you hit trouble - -1. Rebuild from scratch if anything looks stale: `make clean && make`. -2. On Windows, launch `ae` from a directory where your user can write (the first run downloads GCC into `~\.aether\`). -3. Inside an actor body, `state` is a reserved keyword, pick a different local-variable name; outside actor bodies it has no special meaning. - -### WebAssembly and Embedded Targets +## WebAssembly and Embedded Targets Aether supports cross-compilation to platforms without pthreads or POSIX APIs via the `PLATFORM` Makefile variable: @@ -810,7 +333,7 @@ On threadless platforms, the cooperative scheduler (`aether_scheduler_coop.c`) r Stdlib modules that depend on filesystem or networking return errors gracefully (NULL, 0, -1). Console I/O (`print`, `println`) always works. -#### ESP32 (Xtensa) +### ESP32 (Xtensa) The generated C builds as an [ESP-IDF](https://docs.espressif.com/projects/esp-idf/) component for the ESP32 (the chip on boards such as the ESP32-2432S028R "cheap yellow display"): ESP-IDF's `xtensa-esp32-elf-gcc` compiles it with the runtime, using the cooperative scheduler, and the generated C supplies the `app_main()` entry point ESP-IDF calls. `tests/esp32/project/` is a working project; its `main/CMakeLists.txt` lists the runtime sources and the settings an ESP32 needs, notably smaller scheduler tables (`MAX_CORES=1 QUEUE_SIZE=64 SCHEDULER_DRAIN_BATCH=32 MAX_ACTORS_PER_CORE=256`; at the defaults they need about 10 MB, against roughly 320 KB of DRAM). @@ -820,6 +343,38 @@ make ci-esp32 # needs podman or docker; pulls espressif/idf builds a few programs (hello-world, a stateful actor, an actor pipeline, ping-pong) into firmware and boots each in Espressif's QEMU, checking the console output. A program is about 150 KB of firmware. Not yet covered: threads (FreeRTOS has pthreads, so the multicore scheduler is a candidate), the filesystem and networking, and real hardware. +## Troubleshooting + +### Build Failures + +**"Aether compiler not found" (Windows)** +- Make sure you **restarted your terminal** after adding `C:\aether\bin` to PATH +- Verify: `where ae` should show `C:\aether\bin\ae.exe` +- Verify: `where aetherc` should show `C:\aether\bin\aetherc.exe` +- If both exist but it still fails, set `AETHER_HOME`: `set AETHER_HOME=C:\aether` + +**"gcc: command not found" (Windows)** +- This should not happen with the pre-built binary, `ae` auto-downloads GCC (~80 MB) on first run +- If you built from source via MSYS2, open the "MSYS2 MinGW 64-bit" shell, not plain PowerShell +- Verify: `gcc --version` should show MinGW-w64 GCC + +**"pthread.h: No such file or directory"** +- Linux: `sudo apt-get install libpthread-stubs0-dev` +- Windows: The Aether runtime uses Win32 threads natively, no pthread library needed + +**A function from the standard library is undefined** +- `ae run` reads the standard library of the installed toolchain: run `ae version`, and `ae upgrade` when it names a newer release. + +**Test failures (building from source)** +- Run `make test` for unit tests, `make test-ae` for integration tests +- Check for port conflicts if network tests fail (port 8080) + +### Quick tips when you hit trouble + +1. Building from source and anything looks stale: rebuild with `make clean && make`. +2. On Windows, launch `ae` from a directory where your user can write (the first run downloads GCC into `~\.aether\`). +3. Inside an actor body, `state` is a reserved keyword, pick a different local-variable name; outside actor bodies it has no special meaning. + ### Platform-Specific Notes **macOS:** diff --git a/docs/package-fetching.md b/docs/package-fetching.md index 8cabd0cdb..68f78e84a 100644 --- a/docs/package-fetching.md +++ b/docs/package-fetching.md @@ -74,8 +74,68 @@ safe to use: it cannot hand over another tree than the one pinned. A package that publishes a release artifact for the host (a binary package, or `---.tar.gz`) is installed from it instead when a -version is given, verified by its `.sha256` (getting-started.md, "Add a -package"). `--source` skips that and takes the source archive. +version is given, verified by its `.sha256` ("Release artifacts" below). +`--source` skips that and takes the source archive. + +## Release artifacts + +With `@version`, `ae add` prefers a **published release artifact** when the +package ships one for your platform, and takes the source archive otherwise. +The asset is expected at the same layout Aether's own releases use: + +``` +https:///releases/download//---.tar.gz +``` + +(`.zip` is tried too; `-` is `linux-x86_64`, `macos-arm64`, and +so on.) A sibling `.sha256` is verified when present: a +**mismatch is fatal and installs nothing**, and there is no silent +fall-back to the source, which would defeat the check. An artifact with no +published checksum installs with a warning. + +Prebuilt `--emit=lib` artifacts are directly importable: the binary-import +prepass reads the artifact's catalog and synthesizes the interface, so a +released library needs no toolchain to consume. Set +`AE_RELEASE_BASE_URL` to point at an internal mirror serving the same +layout, and the source archives at `/archive/.tar.gz`. + +### Publishing a binary package + +For a **binary package**, publish `aether.toml` with a `[package]` section +containing `binary = ""` and `modules = "."`, plus +`--.` and its +`.sha256` sidecar. Unlike the archive path, a binary package requires the +checksum: a missing or mismatched checksum is fatal. If the declared binary +is missing for the selected target, `ae add` fails without trying another +libc or a source archive. + +Publish Linux binary packages under distinct glibc and musl identities: + +| Build target | Release triple | +| --- | --- | +| `x86_64-linux` (glibc) | `linux-x86_64` | +| `aarch64-linux` (glibc) | `linux-arm64` | +| `x86_64-linux-musl` | `linux-x86_64-musl` | +| `aarch64-linux-musl` | `linux-arm64-musl` | + +Automatic selection uses the CPU and libc that `ae` was built for, retaining +the existing unsuffixed glibc names. To fetch a foreign binary, give its +**release triple** explicitly (rather than the compiler's build-target spelling): + +```bash +ae add github.com/user/repo@v1.0.0 --target linux-arm64-musl +``` + +The explicit target is preserved in the asset name; it does not fall back to +the host's libc. The package cache holds one target per package. Use a separate +`HOME` per target when assembling a multi-target release. `ae add` refuses to +reuse an installed binary for a different target and preserves its bytes. +Older binary caches without a target record must be removed and fetched again +so the selected asset and its checksum are verified. + +For example, a stem of `libfoo` yields +`libfoo-v1.0.0-linux-arm64-musl.so` and +`libfoo-v1.0.0-linux-arm64-musl.so.sha256`. ## The fetcher diff --git a/docs/tutorial.md b/docs/tutorial.md index 3d7984258..2e8b1bb4a 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1,507 +1,716 @@ -# Aether Language Tutorial +# Learn Aether: the tutorial -This tutorial walks through Aether's core features with working examples you can run. Aether is a compiled language built around the **actor model**, lightweight concurrent entities that communicate through messages. +This tutorial is the story told alongside [the path through the +examples](../examples/README.md). The path is the code: one file per idea, +each saying how to run it and ending with what it prints. This page says +what each step shows, why it comes where it does, and what newcomers get +wrong, and each chapter ends with exercises and their answers. + +Read a chapter, then run its examples in order from the repository root +and change them: -Run any example with: ```bash -ae run myfile.ae +ae run examples/01-language/01-hello.ae ``` -## Table of Contents -1. [Hello World](#1-hello-world) -2. [Your First Actor](#2-your-first-actor) -3. [Actor State & Multiple Messages](#3-actor-state--multiple-messages) -4. [Control Flow](#4-control-flow) -5. [Pattern Matching](#5-pattern-matching) -6. [Multiple Actors](#6-multiple-actors) -7. [String Interpolation](#7-string-interpolation) -8. [Constants, Null, and Bitwise](#8-constants-null-and-bitwise) -9. [Ergonomic Syntax](#9-ergonomic-syntax) +Every program on this page is compiled by CI, and the ones followed by +their output are run and compared, so what you read is what Aether does +today. [Getting started](getting-started.md) installs the toolchain; the +[glossary](glossary.md) defines the words this page uses; the [language +reference](language-reference.md) has everything else. + +1. [The language](#1-the-language) +2. [Actors](#2-actors) +3. [The standard library](#3-the-standard-library) +4. [C and other languages](#4-c-and-other-languages) +5. [Containing code](#5-containing-code) +6. [Tools](#6-tools) +7. [Programs](#7-programs) --- -## 1. Hello World +## 1. The language -```aether -main() { - println("Hello, Aether!") +Aether compiles to C: `ae run` turns a file into a native program and runs +it. A program is a set of functions, and it starts at `main()`. There are +no classes and no methods: structs are plain data, behaviour is functions, +and the one stateful thing with an inside and an outside is the actor +(chapter 2). - x = 42 - y = 8 +### Values, strings and control flow - if (x > y) { - println("x is greater than y") - } +[01-hello](../examples/01-language/01-hello.ae), +[02-values-and-types](../examples/01-language/02-values-and-types.ae), +[03-strings](../examples/01-language/03-strings.ae), +[04-control-flow](../examples/01-language/04-control-flow.ae) + +A line ends a statement, and `${...}` puts any value into a string. A local +takes its type from the first value it is given and keeps it. Conditions +take no parentheses, every body takes braces, and `if` is also a value: - sum = x + y - println("Sum: ${sum}") +```aether,run +main() { + temps = [12, 19, 25, 31] + for i in 0..temps.len { + t = temps[i] + feel = if t < 15 { "cold" } else if t < 28 { "mild" } else { "hot" } + println("${t} C is ${feel}") + } } ``` -**What you see:** -- `main()` is the entry point -- Variables use **type inference**, no explicit types needed -- `println` prints with a newline; `print` prints without -- `${variable}` embeds a value into a string +```output +12 C is cold +19 C is mild +25 C is mild +31 C is hot +``` + +A string is immutable text that knows its length; `==` compares contents. +What takes strings apart and builds new ones is in `std.string`, called +through the module's name: `string.length(s)`, `string.to_upper(s)`. ---- +### Functions -## 2. Your First Actor +[05-functions](../examples/01-language/05-functions.ae) -Actors are the core abstraction in Aether, lightweight concurrent entities that process messages. +Parameters can have defaults and calls can name their arguments. A +function can return several values, which the caller takes apart, and a +function can be written as clauses, each for one shape of argument: ```aether,run -message Ping {} +min_max(xs: int[]) -> (int, int) { + lo = xs[0] + hi = xs[0] + for i in 1..xs.len { + if xs[i] < lo { lo = xs[i] } + if xs[i] > hi { hi = xs[i] } + } + return lo, hi +} -actor Counter { - state count = 0 +describe(0) -> "zero" +describe(n) when n < 0 -> "negative" +describe(n) -> "positive" - receive { - Ping() -> { - count = count + 1 - println("Count: ${count}") - } - } +greet(name: string, greeting: string = "Hello") -> string { + return "${greeting}, ${name}" } main() { - c = spawn(Counter()) - c ! Ping {} - c ! Ping {} - c ! Ping {} - - wait_for_idle() + lo, hi = min_max([7, -2, 9, 4]) + println("${lo} .. ${hi}") + println("${describe(0)} ${describe(-5)} ${describe(3)}") + println(greet("Ada")) + println(greet(greeting: "Welcome", name: "Grace")) } ``` ```output -Count: 1 -Count: 2 -Count: 3 +-2 .. 9 +zero negative positive +Hello, Ada +Welcome, Grace ``` -**Key concepts:** -- `message Ping {}` defines a message type (empty here, but can have fields) -- `actor Counter { state ... receive { ... } }` an actor with private state -- `spawn(Counter())` creates a Counter actor; returns a handle -- `c ! Ping {}` sends an asynchronous message -- `wait_for_idle()` blocks until all actors have finished processing +### Data: structs, enums, sum types and match -Without `wait_for_idle()`, main could exit before the actor processes its messages. - ---- +[06-structs](../examples/01-language/06-structs.ae), +[07-enums-and-sums](../examples/01-language/07-enums-and-sums.ae), +[08-match](../examples/01-language/08-match.ae) -## 3. Actor State & Multiple Messages - -Actors can handle multiple message types and carry state across messages: +A struct is a record held by value: assigning or passing one copies it, +and a function that should change the caller's takes its address, `&p`. +An enum is a set of named constants. A sum type is a value that is exactly +one of several structs, and `match` over it must cover every one, so +adding a variant shows every place that has to learn it: ```aether,run -message Increment { amount: int } -message Decrement { amount: int } -message Reset {} -message GetValue {} +struct Deposit { amount: int } +struct Withdraw { amount: int } +struct Close {} -actor Counter { - state count = 0 +type Command = Deposit | Withdraw | Close - receive { - Increment(amount) -> { - count = count + amount - } - Decrement(amount) -> { - count = count - amount - } - Reset() -> { - count = 0 - } - GetValue() -> { - println("Current value: ${count}") - } +apply(balance: int, c: Command) -> int { + return match c { + Deposit -> balance + c.amount + Withdraw -> balance - c.amount + Close -> 0 } } main() { - c = spawn(Counter()) - c ! Increment { amount: 10 } - c ! Increment { amount: 5 } - c ! GetValue {} - c ! Decrement { amount: 3 } - c ! GetValue {} - c ! Reset {} - c ! GetValue {} - - wait_for_idle() + let cmds: Command[3] = [Deposit { amount: 50 }, Withdraw { amount: 20 }, Deposit { amount: 5 }] + balance = 0 + for i in 0..cmds.len { + balance = apply(balance, cmds[i]) + } + println("balance: ${balance}") } ``` ```output -Current value: 15 -Current value: 12 -Current value: 0 +balance: 35 ``` -Messages are processed **in order** within a single actor. State (`count`) is private, nothing outside the actor can access it directly. +Inside an arm, the value is that variant, so `c.amount` reads its field. +`match` also takes literals, ranges, strings, list patterns such as +`[head | tail]` and struct patterns; it is an expression, so it has a value. ---- +### Absence and failure -## 4. Control Flow +[09-optionals](../examples/01-language/09-optionals.ae), +[10-errors](../examples/01-language/10-errors.ae) -Aether has standard control structures: +Two different things, two different tools. A value that may be absent is +an optional, `T?`: `none` or a value, read with `??` for a fallback. An +operation that can fail returns its value and an error string, `""` when +it worked; `-> T!` names that pair, and `or` gives a fallback: ```aether,run -main() { - // For loops (with compound assignment operators) - sum = 0 - for (i = 1; i <= 10; i++) { - sum += i - } - println("Sum 1..10 = ${sum}") - - // While loops - n = 16 - steps = 0 - while (n > 1) { - if (n % 2 == 0) { - n = n / 2 - } else { - n = n * 3 + 1 +import std.string + +first_even(xs: int[]) -> int? { + for i in 0..xs.len { + if xs[i] % 2 == 0 { + return xs[i] } - steps = steps + 1 } - println("Collatz steps for 16: ${steps}") - - // If / else if / else - score = 85 - if (score >= 90) { - println("Grade: A") - } else if (score >= 80) { - println("Grade: B") - } else { - println("Grade: C or below") + return none +} + +parse_percent(text: string) -> int! { + n, err = string.to_int(text) + if err != "" { + return 0, "'${text}' is not a number" + } + if n < 0 || n > 100 { + return 0, "${n} is not a percentage" } + return n +} + +main() { + println(first_even([3, 8, 5]) ?? -1) + println(first_even([3, 5]) ?? -1) + + v, err = parse_percent("120") + println("${v} [${err}]") + println(parse_percent("42") or 0) + println(parse_percent("lots") or 0) } ``` ```output -Sum 1..10 = 55 -Collatz steps for 16: 4 -Grade: B +8 +-1 +0 [120 is not a percentage] +42 +0 ``` ---- +A panic is neither: it is for a bug. It unwinds to the nearest `try`, and +outside one it ends the program. A bad index, a broken contract and a +`none` forced open with `!` are panics. -## 5. Pattern Matching +### Arrays, slices and collections -Pattern matching, inspired by Erlang, lets you dispatch on argument values and structure. +[11-arrays-and-slices](../examples/01-language/11-arrays-and-slices.ae), +[12-collections](../examples/01-language/12-collections.ae) -### Function Clauses +`[1, 2, 3]` is a fixed array, a value like a struct. A slice, `int[]`, is +a view of elements stored elsewhere, so a parameter typed `int[]` takes any +array without copying it. Every index is checked. For things that grow, +`std.list`, `std.map` and `std.set` give handles (below). -Define functions with multiple clauses that match on argument values: +### Functions as values, and modules -```aether,run -// Recursive factorial with pattern matching -factorial(0) -> 1 -factorial(n) when n > 0 -> n * factorial(n - 1) +[13-closures](../examples/01-language/13-closures.ae), +[14-modules](../examples/01-language/14-modules/main.ae) + +`|x| -> x * 2` is a function value, and a closure sees the variables +around it. A call followed by a block passes the block as its last +argument: the trailing-block form that most of std's builders use. +A module is a directory under `lib/` with a `module.ae` whose +`exports(...)` line names what other files may use. + +### Contracts, defer and ownership + +[15-contracts](../examples/01-language/15-contracts.ae), +[16-defer](../examples/01-language/16-defer.ae), +[17-ownership](../examples/01-language/17-ownership.ae) -// Fibonacci -fib(0) -> 0 -fib(1) -> 1 -fib(n) when n > 1 -> fib(n - 1) + fib(n - 2) +`requires` and `ensures` state what a function needs and what it +promises, and a broken one is a panic naming the clause. `defer` runs its +statement however the block ends. -// Classify a number -classify(x) when x < 0 -> "negative" -classify(x) when x == 0 -> "zero" -classify(x) when x > 0 -> "positive" +Ownership is the last step of the chapter because it explains the others. +Aether has no garbage collector, and it does not ask you to free +everything either. Strings, closures, and the structs, arrays and +optionals made from them are counted: the compiler frees them, and you +write no free. A *handle*, a `ptr` a module gave you (a list, a map, a +JSON document, a buffer), is the program's to free, through its module, +and `defer` keeps the free next to the `new`: + +```aether,run +import std.list main() { - println("5! = ${factorial(5)}") - println("fib(8) = ${fib(8)}") - println("classify(-3) = ${classify(-3)}") - println("classify(0) = ${classify(0)}") - println("classify(7) = ${classify(7)}") + names = list.new() + defer list.free(names) + list.add(names, "ada") + list.add(names, "grace") + println("${list.size(names)} names") } ``` ```output -5! = 120 -fib(8) = 21 -classify(-3) = negative -classify(0) = zero -classify(7) = positive +2 names ``` -### Match Statements +### Common mistakes -For value dispatch inside functions or message handlers: +- **Freeing a value the compiler counts.** A string needs no free, and + freeing a parameter the function only borrows is refused (E0901). Free + handles, and only handles. +- **Forgetting a handle.** A `list.new()` with no `list.free` leaks it. + Write the `defer` on the next line. +- **Changing a local's type.** `count = 0` then `count = "many"` is a + compile error, not a new variable. +- **A panic for an expected failure.** Bad input from a user is an error + to return (`T!`), not a panic; panics are for bugs. +- **Reading an element of a list or map as a string without its getter.** + Elements come back untyped; `list.get_string` and `map.get_string` say + when one is not a string. -```aether,run -message Process { code: int } +### Exercises -actor Dispatcher { - receive { - Process(code) -> { - match (code) { - 0 -> { println("OK") } - 1 -> { println("Warning") } - 2 -> { println("Error") } - _ -> { println("Unknown code: ${code}") } - } - } - } +**1. FizzBuzz.** Print 1 to 15, but "Fizz" for multiples of 3, "Buzz" for +multiples of 5, and "FizzBuzz" for both. + +
+An answer + +```aether,run +fizzbuzz(n: int) -> string { + if n % 15 == 0 { return "FizzBuzz" } + if n % 3 == 0 { return "Fizz" } + if n % 5 == 0 { return "Buzz" } + return "${n}" } main() { - d = spawn(Dispatcher()) - d ! Process { code: 0 } - d ! Process { code: 2 } - d ! Process { code: 99 } - wait_for_idle() + line = "" + for i in 1..16 { + line = "${line}${fizzbuzz(i)} " + } + println(line) } ``` ```output -OK -Error -Unknown code: 99 +1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz ``` ---- +
-## 6. Multiple Actors +**2. The largest.** Write `largest(xs: int[]) -> int?`, `none` for an empty +slice, and print the largest of `[4, 11, 7]` and of `[]`, with "nothing" +for the second. -Actors can be spawned in any number and run concurrently across all CPU cores: +
+An answer -```aether -message Work { id: int, value: int } -message Done {} - -actor Worker { - state processed = 0 +```aether,run +largest(xs: int[]) -> int? { + if xs.len == 0 { + return none + } + best = xs[0] + for i in 1..xs.len { + if xs[i] > best { best = xs[i] } + } + return best +} - receive { - Work(id, value) -> { - result = value * value - println("Worker ${id}: ${value}^2 = ${result}") - processed = processed + 1 - } - Done() -> { - println("Worker finished ${processed} tasks") - } +show(v: int?) -> string { + return match v { + none -> "nothing" + some(n) -> "${n}" } } main() { - w1 = spawn(Worker()) - w2 = spawn(Worker()) - w3 = spawn(Worker()) - - // Distribute work across workers - w1 ! Work { id: 1, value: 3 } - w2 ! Work { id: 2, value: 5 } - w3 ! Work { id: 3, value: 7 } - w1 ! Work { id: 1, value: 9 } - w2 ! Work { id: 2, value: 11 } - - w1 ! Done {} - w2 ! Done {} - w3 ! Done {} - - wait_for_idle() + println(show(largest([4, 11, 7]))) + println(show(largest([]))) } ``` -The Aether runtime automatically distributes actors across CPU cores. Message ordering is guaranteed **within** a single actor, but messages to different actors may be processed in any order. - ---- - -## 7. String Interpolation +```output +11 +nothing +``` -Use `${expression}` to embed values in strings: +
-```aether -main() { - name = "Aether" - version = 1 - pi = 3 +**3. A result that can fail.** Write `divide(a: int, b: int) -> int!` that +fails with "division by zero", and print `divide(9, 3)` and, with `or` and +a fallback of -1, `divide(1, 0)`. - println("Welcome to ${name} v${version}!") - println("Pi is approximately ${pi}") +
+An answer - for (i = 1; i <= 5; i = i + 1) { - squared = i * i - println("${i}^2 = ${squared}") +```aether,run +divide(a: int, b: int) -> int! { + if b == 0 { + return 0, "division by zero" } + return a / b } -``` -String interpolation works with `println` and any function that accepts a string. Variables of any type (int, string) can be interpolated. - -Interpolated strings produce a real string value when assigned to a variable, so they can be passed to any function: +main() { + q, err = divide(9, 3) + println("${q} [${err}]") + println(divide(1, 0) or -1) +} +``` -```aether,fragment -msg = "Hello, ${name}!" // msg owns the new string; it is released with msg +```output +3 [] +-1 ``` ---- +
-## 8. Constants, Null, and Bitwise +--- -### Constants +## 2. Actors -Define named values at the top level with `const`: +Aether's concurrency is actors: a piece of state that only messages reach. +Nothing outside an actor touches its state; other code sends it a message, +and it changes its own state, one message at a time. So there are no locks +to take and no data races to find. The scheduler runs actors on every +core, so many actors use the whole machine. -```aether -const WIDTH = 80 -const HEIGHT = 24 -const TITLE = "My App" +### A first actor, its state, and asking it -main() { - println(TITLE) - area = WIDTH * HEIGHT - println("Area: ${area}") -} -``` +[01-first-actor](../examples/02-actors/01-first-actor.ae), +[02-state](../examples/02-actors/02-state.ae), +[03-ask-reply](../examples/02-actors/03-ask-reply.ae) -### Null +`message` declares what can be sent, `actor` what an actor does with each +kind, `spawn` starts one, `!` sends without waiting, and `?` sends and +waits for the handler's `reply`: -Use `null` for uninitialized pointers: +```aether,run +message Add { n: int } +message Total {} -```aether -main() { - conn = null - if conn == null { - println("Not connected") +actor Counter { + state total = 0 + receive { + Add(n) -> { total = total + n } + Total() -> { reply total } } } -``` -### Bitwise Operators - -Aether supports `&`, `|`, `^`, `~`, `<<`, `>>` for bit manipulation: - -```aether main() { - flags = 5 & 3 // AND: 1 - mask = 5 | 3 // OR: 7 - flipped = 5 ^ 3 // XOR: 6 - shifted = 1 << 4 // Left shift: 16 - println("${flags} ${mask} ${flipped} ${shifted}") + c = spawn(Counter()) + c ! Add { n: 5 } + c ! Add { n: 7 } + int t = c ? Total {} + println("total: ${t}") } ``` ---- +```output +total: 12 +``` -## 9. Ergonomic Syntax +The ask sees both adds: messages from one sender reach an actor in the +order they were sent. `wait_for_idle()` is the other way to wait: it +returns once every actor has nothing left to do, which is how a program +lets its actors finish before it ends. + +### Actors that talk to each other + +[04-actor-refs](../examples/02-actors/04-actor-refs.ae), +[05-messages](../examples/02-actors/05-messages.ae), +[06-pipeline](../examples/02-actors/06-pipeline.ae), +[07-worker-pool](../examples/02-actors/07-worker-pool.ae) + +What `spawn` returns is an `actor_ref`, a value that goes into messages and +state, so an actor can be told whom to answer; `self` is an actor's own. +A message owns the strings and arrays in it, so the sender can reuse its +variables at once; a `ptr` in a message hands the handle over. With refs +come the shapes of concurrent programs: a pipeline of stages, and a pool +of workers whose results a collector gathers. Order holds between one +sender and one receiver, never between two senders. + +### Shapes of an actor's life + +[08-state-machine](../examples/02-actors/08-state-machine.ae), +[09-timeouts](../examples/02-actors/09-timeouts.ae) + +An enum as an actor's state, with a transition table, makes a state +machine that applies one event at a time. `receive { ... } after N -> { ... }` +does something when no message has come for N milliseconds: a watchdog. + +### When actors fail + +[10-crash-isolation](../examples/02-actors/10-crash-isolation.ae), +[11-ending](../examples/02-actors/11-ending.ae), +[12-links](../examples/02-actors/12-links.ae), +[13-supervisor](../examples/02-actors/13-supervisor.ae) + +A panic in a handler ends that actor and nothing else. An actor also ends +when it stops itself (`actors.stop()`) or something kills it +(`actors.kill(ref)`), and whoever monitors it gets a `Down` with the +reason. A link ties two actors so that one's failure ends the other, +unless that one traps exits and takes the `Down` instead. A supervisor is +built from those: it links its children, and when one fails it restarts +it in place, so every ref to the child reaches the new one. + +### Holding a producer back + +[14-bounded-mailbox](../examples/02-actors/14-bounded-mailbox.ae), +[15-streams](../examples/02-actors/15-streams.ae), +[16-sharded-map](../examples/02-actors/16-sharded-map.ae) + +A send never blocks, so an actor that receives faster than it handles +grows its mailbox without bound. `@mailbox(bound: N)` caps it: past N, a +send is refused at once, and `try_send` says so. `std.stream` bounds a flow +between two actors with credits, so a fast producer never has more than a +window of chunks in flight. And when one actor's mailbox is the +bottleneck, its work can be split over several by key. + +### Common mistakes + +- **Reading an actor's state from outside.** Ask it (`?`), or have it send + what it knows; its state is its own. +- **Expecting order between two senders.** Main and another actor sending + to the same actor interleave in any order. Wait for one before the + other, or route both through one sender. +- **Calling `wait_for_idle()` inside an actor.** It waits for every actor, + this one too, so it can never return there; it panics instead. Call it + from main. +- **Blocking in a handler.** A handler that sleeps or waits on a socket + holds a core. Keep handlers short, and give slow work its own actor. +- **Using a handle after sending it.** A `ptr` sent in a message belongs to + the receiver now; the sender must not touch it again. + +### Exercises + +**1. A bank account.** An actor with a balance takes `Deposit { n }` and +`Withdraw { n }`, refusing a withdrawal larger than the balance, and +answers `Balance {}`. Deposit 100, withdraw 30, then try to withdraw 500, +and print the balance. + +
+An answer -Aether includes several features that reduce boilerplate. +```aether,run +message Deposit { n: int } +message Withdraw { n: int } +message Balance {} -### Hex, Octal, and Binary Literals +actor Account { + state balance = 0 + receive { + Deposit(n) -> { balance = balance + n } + Withdraw(n) -> { + if n > balance { + println("refused: ${n} is more than ${balance}") + } else { + balance = balance - n + } + } + Balance() -> { reply balance } + } +} -```aether main() { - flags = 0xFF - mask = 0x0F - bits = 0b1010_0101 - perms = 0o755 - - println(flags & mask) // 15 - println(bits >> 4) // 10 + a = spawn(Account()) + a ! Deposit { n: 100 } + a ! Withdraw { n: 30 } + a ! Withdraw { n: 500 } + int b = a ? Balance {} + println("balance: ${b}") } ``` -### If-Expressions +```output +refused: 500 is more than 70 +balance: 70 +``` -Use `if`/`else` as a value-producing expression. The `else` is required, and -the branches must have a type in common (an `int` and a `string` do not): +
-```aether -main() { - x = 10 - sign = if x > 0 { 1 } else { -1 } - println(sign) // 1 -} -``` +**2. A two-stage pipeline.** A `Doubler` doubles each number it gets and +sends the result on to a `Printer`, which prints it. Send 1, 2 and 3 +through. -### Range-Based For Loops +
+An answer -```aether -main() { - // Instead of: for (i = 0; i < 10; i++) { ... } - for i in 0..10 { - print(i) - print(" ") +```aether,run +message Num { n: int } +message Connect { next: actor_ref } + +actor Printer { + receive { + Num(n) -> { println("got ${n}") } } - // prints: 0 1 2 3 4 5 6 7 8 9 } -``` -To visit the elements of a slice or array rather than a count, loop over it directly: +actor Doubler { + state next: actor_ref = null + receive { + Connect(next: n) -> { next = n } + Num(n) -> { next ! Num { n: n * 2 } } + } +} -```aether main() { - for word in ["a", "b", "c"] { - print(word) + printer = spawn(Printer()) + doubler = spawn(Doubler()) + doubler ! Connect { next: printer } + for i in 1..4 { + doubler ! Num { n: i } } - // prints: abc + wait_for_idle() } ``` -### Multi-Statement Arrow Bodies +```output +got 2 +got 4 +got 6 +``` -Arrow functions can contain multiple statements. The last expression is the implicit return: +
-```aether -distance(x1, y1, x2, y2) -> { - dx = x2 - x1 - dy = y2 - y1 - dx * dx + dy * dy -} +--- -main() { - println(distance(0, 0, 3, 4)) // 25 -} -``` +## 3. The standard library + +[01-files](../examples/03-std/01-files.ae) · +[02-json](../examples/03-std/02-json.ae) · +[03-processes](../examples/03-std/03-processes.ae) · +[04-http](../examples/03-std/04-http.ae) · +[05-http-server](../examples/03-std/05-http-server.ae) · +[06-http2-tls](../examples/03-std/06-http2-tls.ae) · +[07-reverse-proxy](../examples/03-std/07-reverse-proxy.ae) · +[08-proxy-pool](../examples/03-std/08-proxy-pool.ae) · +[09-logging](../examples/03-std/09-logging.ae) · +[10-tcp](../examples/03-std/10-tcp.ae) · +[11-cryptography](../examples/03-std/11-cryptography.ae) + +The standard library follows the two rules of chapter 1 everywhere. A call +that can fail returns its value and an error string, so a program checks +`err` and goes on. What a module hands out as a `ptr` (a JSON document, a +server, a socket) is a handle the program frees through that module. + +- **Files and processes.** `std.fs` reads, writes and walks the file + system. `std.os` runs a program from an argument list, with no shell in + between, and reads what it prints and its status. +- **Data.** `std.json` parses into a tree the program frees once, and + builds documents to encode. +- **The network.** `std.http` serves by route and fetches: the same server + API carries middleware, health probes and metrics, HTTP/2 and TLS, and + `std.http.proxy` forwards to upstreams. Under it, `std.tcp` is the + socket: a byte stream, so a protocol says where a message ends. +- **Logs and secrets.** `std.log` writes levelled lines. The + `std.cryptography` modules are written in Aether: hashes, HMACs, + signatures, and the pure-Aether TLS. + +The [standard library reference](stdlib-reference.md) lists every module +and call. --- -## Next Steps +## 4. C and other languages + +[01-libc](../examples/04-interop/01-libc.ae) · +[02-own-c](../examples/04-interop/02-own-c/main.ae) · +[03-callbacks](../examples/04-interop/03-callbacks.ae) · +[04-embedding](../examples/04-interop/04-embedding/greeter.ae) · +[05-hosts](../examples/04-interop/05-hosts/python.ae) · +[06-java-host](../examples/04-interop/06-java-host/aether/manifest.ae) · +[07-limits](../examples/04-interop/07-limits/script.ae) + +Aether compiles to C, so C is one declaration away in both directions. +`extern` declares a C function to call; `@source` ships a C file with a +module; `@c_callback` hands an Aether function to C as a function pointer. +The other way, `ae build --emit=obj` (or `--emit=lib`) makes an Aether +file a library a C program links, each function an `aether_` +export. + +A program that runs Aether code it does not control bounds it: every +export runs in an execution frame, so a deadline, a cancel from another +thread or a memory budget ends a call and gives the host an outcome, +never a crash. And Aether can host other languages (Python, Lua, Tcl, Go) +inside the program, under the sandbox of chapter 5. [C +interoperability](c-interop.md) and [emit=lib](emit-lib.md) are the +references. -### Best Practices +--- -1. **One responsibility per actor**, keep actors focused on a single task -2. **No shared state**, communicate only through messages, never share memory -3. **Always `wait_for_idle()`** when you need to read actor state or coordinate completion -4. **Descriptive message names**, `Increment` is clearer than `Update` +## 5. Containing code -### Further Reading +[01-sandbox](../examples/05-containment/01-sandbox.ae) · +[02-trusted-calls](../examples/05-containment/02-trusted-calls.ae) · +[03-audit](../examples/05-containment/03-audit.ae) · +[04-spawn-sandboxed](../examples/05-containment/04-spawn-sandboxed.ae) · +[05-capsicum](../examples/05-containment/05-capsicum.ae) · +[06-casper](../examples/05-containment/06-casper.ae) -- [Getting Started](getting-started.md), Installation and project setup -- [Language Reference](language-reference.md), Complete syntax specification -- [Standard Library](stdlib-reference.md), Collections, I/O, networking -- [Architecture](architecture.md), Compiler and runtime internals -- [Runtime Optimizations](runtime-optimizations.md), Performance details +A sandbox is a list of grants: files it may read or write, environment +variables, programs, network hosts. `sandbox.enforce(grants) callback { ... }` +runs a block under them, and anything not granted is refused, the +refusal recorded where `std.audit` can read it back. A trusted call lets +the block use one service with your authority. A child process can be held +to grants at its own libc calls, and on FreeBSD, Capsicum has the kernel +enforce it. [The containment sandbox](containment-sandbox.md) is the +reference. --- -## Summary - -| Feature | Syntax | -|---------|--------| -| Define message | `message Name { field: type }` | -| Define actor | `actor Name { state x = 0; receive { ... } }` | -| Spawn actor | `a = spawn(ActorName())` | -| Send message | `a ! MessageName { field: value }` | -| Wait for all | `wait_for_idle()` | -| Pattern clause | `fn(0) -> result` | -| Guard | `fn(n) when n > 0 -> ...` | -| Match | `match (x) { 0 -> {...} _ -> {...} }` | -| Interpolation | `"text ${variable}"` | -| Constant | `const NAME = value` | -| Null | `x = null` | -| Bitwise | `&` `\|` `^` `~` `<<` `>>` | -| Compound assign | `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` | -| Hex/bin/oct | `0xFF`, `0b1010`, `0o755` | -| If-expression | `if cond { a } else { b }` | -| Range for | `for i in 0..10 { ... }` | -| For-in (slice/array) | `for x in xs { ... }` | -| Arrow block | `f(x) -> { stmts; expr }` | +## 6. Tools + +[01-testing](../examples/06-tooling/01-testing/discount_test.ae) · +[02-mutation](../examples/06-tooling/02-mutation/mutate.ae) · +[03-project](../examples/06-tooling/03-project/src/main.ae) · +[04-lib-path](../examples/06-tooling/04-lib-path/main.ae) · +[05-ae-help](../examples/06-tooling/05-ae-help/broken.ae) + +`std.spec` writes tests (`describe`, `it`, assertions) and `ae test` finds +and runs them; `std.mutation` checks that the tests would notice a change +to the code. A project is a directory with an `aether.toml`, its program in +`src/` and its modules in `lib/`. `ae help` explains what the compiler +found in a script and suggests the fix. [Testing](testing.md) and +[the formatter](formatter.md) cover the rest of the everyday tools. + +--- + +## 7. Programs + +[01-word-count](../examples/07-applications/01-word-count/main.ae) · +[02-chat-room](../examples/07-applications/02-chat-room.ae) · +[03-build-supervisor](../examples/07-applications/03-build-supervisor.ae) · +[04-calculator](../examples/07-applications/04-calculator.ae) + +The last part puts the others together: a word counter (files, sorting, +errors), a chat room (actors keeping each other in state), a build +supervisor (a process with a deadline and nothing left behind) and a +terminal calculator (a builder DSL with closures). Read them for how the +pieces fit, then write your own. + +## Where next + +- [Language reference](language-reference.md): every construct, in full. +- [Memory management](memory-management.md): the ownership rules. +- [Actor concurrency](actor-concurrency.md): how the runtime delivers, + orders and ends. +- [Glossary](glossary.md): the words, defined. diff --git a/docs/tutorials/01-hello-aether.md b/docs/tutorials/01-hello-aether.md deleted file mode 100644 index 7a7ac3584..000000000 --- a/docs/tutorials/01-hello-aether.md +++ /dev/null @@ -1,286 +0,0 @@ -# Tutorial 1: Hello, Aether! - -**Time:** 30 minutes -**Prerequisites:** Basic programming knowledge -**Goal:** Write and run your first Aether program - -## Introduction - -Welcome to Aether! In this tutorial, you'll learn the basics of the Aether programming language and write your first program. - -## What is Aether? - -Aether is a programming language that combines: -- **Erlang's concurrency model** - Lightweight actors with message passing -- **Type inference** - Automatic type deduction for local variables -- **C's performance** - Compiles to optimized C code with zero runtime overhead - -## Installation - -If you haven't installed Aether yet, follow the installation guide: - -```bash -git clone https://github.com/aether-lang-dev/aether.git -cd aether - -# Build the ae CLI tool (includes compiler) -make ae -``` - -Verify installation: -```bash -./build/ae version -``` - -## Your First Program - -### Step 1: Create a File - -Create a file called `hello.ae`: - -```aether -main() { - print("Hello, Aether!\n") -} -``` - -**What's happening here?** -- `main()` is the entry point of every Aether program -- `print()` outputs text to the console -- `\n` adds a newline - -### Step 2: Run It - -```bash -./build/ae run hello.ae -``` - -You should see: -``` -Hello, Aether! -``` - -**Behind the scenes:** -1. Aether compiles `hello.ae` to C code -2. GCC compiles the C code to an executable -3. The program runs immediately - -## Variables and Type Inference - -Aether automatically figures out types - you don't need to declare them! - -```aether -main() { - // Numbers - x = 42 // inferred as int - pi = 3.14 // inferred as float - - // Text - name = "Alice" // inferred as string - - // Print them - print("x = ") - print(x) - print("\n") - - print("pi = ") - print(pi) - print("\n") - - print("name = ") - print(name) - print("\n") -} -``` - -**Try it:** -```bash -./build/ae run variables.ae -``` - -## Basic Math - -```aether -main() { - a = 10 - b = 5 - - sum = a + b - diff = a - b - product = a * b - quotient = a / b - - print("Sum: ") - print(sum) - print("\n") - - print("Product: ") - print(product) - print("\n") -} -``` - -## Comments - -```aether -main() { - // This is a single-line comment - print("Hello!\n") // Comment after code - - /* - * This is a multi-line comment - * Useful for longer explanations - */ -} -``` - -## Exercises - -### Exercise 1: Personal Greeting - -Create a program that prints your name and age: - -```aether -main() { - name = "YourName" - age = 25 - - // Your code here - // Print: "My name is [name] and I am [age] years old." -} -``` - -
-Solution - -```aether -main() { - name = "Alice" - age = 25 - - print("My name is ") - print(name) - print(" and I am ") - print(age) - print(" years old.\n") -} -``` -
- -### Exercise 2: Temperature Converter - -Convert Celsius to Fahrenheit using the formula: F = (C × 9/5) + 32 - -```aether -main() { - celsius = 25 - - // Calculate fahrenheit - fahrenheit = (celsius * 9 / 5) + 32 - - // Print result - print(celsius) - print(" Celsius = ") - print(fahrenheit) - print(" Fahrenheit\n") -} -``` - -### Exercise 3: Circle Area - -Calculate the area of a circle with radius 5. -Formula: Area = π × r² - -```aether -main() { - pi = 3.14159 - radius = 5 - - // Calculate area - area = pi * radius * radius - - print("Circle area: ") - print(area) - print("\n") -} -``` - -## Common Mistakes - -### Mistake 1: Forgetting Parentheses - -```aether,fails -// Wrong -main { - print("Hello") -} - -// Correct -main() { - print("Hello") -} -``` - -### Mistake 2: Missing Newlines - -```aether,fragment -// Prints on same line -print("Hello") -print("World") -// Output: HelloWorld - -// Add \n for newlines -print("Hello\n") -print("World\n") -// Output: -// Hello -// World -``` - -## Next Steps - -You've learned: -- How to run Aether programs -- Variables and type inference -- Basic math operations -- Comments - -**Next Tutorial:** [Functions and Control Flow](02-functions-and-control-flow.md) - -## Quick Reference - -```aether,fragment -// Variables (type inferred) -x = 42 -name = "Alice" -pi = 3.14 - -// Math -sum = a + b -diff = a - b -product = a * b -quotient = a / b - -// Output -print("Text\n") -print(variable) - -// Comments -// Single line -/* Multi - line */ - -// Entry point -main() { - // Your code here -} -``` - -## Help - -- **Compiler errors?** Check for typos and missing parentheses -- **Program won't run?** Verify GCC is installed: `gcc --version` -- **Questions?** Open an issue on GitHub - -Happy coding! - diff --git a/docs/tutorials/02-functions-and-control-flow.md b/docs/tutorials/02-functions-and-control-flow.md deleted file mode 100644 index d698bc06b..000000000 --- a/docs/tutorials/02-functions-and-control-flow.md +++ /dev/null @@ -1,637 +0,0 @@ -# Tutorial 2: Functions and Control Flow - -**Time:** 1 hour -**Prerequisites:** Tutorial 1 -**Goal:** Master functions, conditionals, and loops - -## Functions - -Functions let you organize and reuse code. - -### Basic Function - -```aether -greet(name) { - print("Hello, ") - print(name) - print("!\n") -} - -main() { - greet("Alice") - greet("Bob") -} -``` - -**Output:** -``` -Hello, Alice! -Hello, Bob! -``` - -### Functions with Return Values - -```aether -add(a, b) { - return a + b -} - -multiply(x, y) { - return x * y -} - -main() { - sum = add(10, 20) - product = multiply(5, 6) - - print("Sum: ") - print(sum) - print("\n") - - print("Product: ") - print(product) - print("\n") -} -``` - -### Type Inference in Functions - -Aether infers types from how you use the function: - -```aether -// Parameters and return type inferred! -square(n) { - return n * n -} - -main() { - result = square(5) // Inferred as: int -> int - print(result) // 25 - print("\n") -} -``` - -### Recursive Functions - -```aether -factorial(n) { - if (n <= 1) { - return 1 - } - return n * factorial(n - 1) -} - -main() { - result = factorial(5) // 120 - print("5! = ") - print(result) - print("\n") -} -``` - -## Control Flow - -### If Statements - -```aether -check_age(age) { - if (age >= 18) { - print("Adult\n") - } else { - print("Minor\n") - } -} - -main() { - check_age(25) // Adult - check_age(15) // Minor -} -``` - -### Comparison Operators - -```aether -main() { - x = 10 - y = 20 - - if (x == y) { print("Equal\n") } - if (x != y) { print("Not equal\n") } - if (x < y) { print("x is less\n") } - if (x > y) { print("x is greater\n") } - if (x <= y) { print("x is less or equal\n") } - if (x >= y) { print("x is greater or equal\n") } -} -``` - -### Nested If - -```aether -classify_number(n) { - if (n > 0) { - if (n > 100) { - print("Large positive\n") - } else { - print("Small positive\n") - } - } else { - if (n < -100) { - print("Large negative\n") - } else { - print("Small negative or zero\n") - } - } -} - -main() { - classify_number(150) // Large positive - classify_number(50) // Small positive - classify_number(-150) // Large negative - classify_number(-5) // Small negative or zero -} -``` - -## Loops - -### While Loop - -```aether -main() { - i = 0 - while (i < 5) { - print(i) - print(" ") - i = i + 1 - } - print("\n") -} -``` - -**Output:** `0 1 2 3 4` - -### For Loop - -```aether -main() { - for (i = 0; i < 5; i = i + 1) { - print(i) - print(" ") - } - print("\n") -} -``` - -**Output:** `0 1 2 3 4` - -### Counting Down - -```aether -main() { - for (i = 5; i > 0; i = i - 1) { - print(i) - print(" ") - } - print("Liftoff!\n") -} -``` - -**Output:** `5 4 3 2 1 Liftoff!` - -## Practical Examples - -### Example 1: Find Maximum - -```aether -max(a, b) { - if (a > b) { - return a - } else { - return b - } -} - -main() { - x = 42 - y = 17 - - biggest = max(x, y) - print("Maximum: ") - print(biggest) - print("\n") -} -``` - -### Example 2: Sum of Numbers - -```aether -sum_to_n(n) { - total = 0 - for (i = 1; i <= n; i = i + 1) { - total = total + i - } - return total -} - -main() { - result = sum_to_n(10) // 1+2+3+...+10 = 55 - print("Sum 1 to 10: ") - print(result) - print("\n") -} -``` - -### Example 3: Fibonacci Sequence - -```aether -fibonacci(n) { - if (n <= 1) { - return n - } - return fibonacci(n - 1) + fibonacci(n - 2) -} - -main() { - print("First 10 Fibonacci numbers:\n") - for (i = 0; i < 10; i = i + 1) { - print(fibonacci(i)) - print(" ") - } - print("\n") -} -``` - -**Output:** `0 1 1 2 3 5 8 13 21 34` - -## Functional Style: Pattern Matching + Recursion - -Aether is inspired by Erlang and supports a functional programming style using pattern matching and recursion. This can often lead to cleaner, more expressive code. - -### Two Ways to Solve Problems - -Here are several examples showing the same problem solved both ways: - -#### Sum of List - -**Imperative style (for loop):** -```aether,fragment -sum_loop(nums, len) { - total = 0 - for (i = 0; i < len; i = i + 1) { - total = total + nums[i] - } - return total -} -``` - -**Functional style (recursion + pattern matching):** -```aether -import std.string - -// A *StringSeq is a cons-cell list (head + tail). `[]` matches the -// empty list; `[h | t]` binds the head `h` and the tail `t`, so the -// function can recurse on the tail. -total_length(words: *StringSeq) { - match words { - [] -> { return 0 } - [h | t] -> { return string.length(h) + total_length(t) } - } - return 0 -} - -main() { - words = string.seq_empty() - words = string.seq_cons("hi", words) - words = string.seq_cons("hello", words) - words = string.seq_cons("hey", words) - println(total_length(words)) // 10 -} // words is released here -``` - -The functional style reads like a definition: "The total length of an empty list is 0; the total length of a list is the length of the head plus the total length of the tail." - -#### Factorial - -**Imperative style:** -```aether,fragment -factorial_loop(n) { - result = 1 - for (i = 2; i <= n; i = i + 1) { - result = result * i - } - return result -} -``` - -**Functional style:** -```aether,fragment -factorial(n) { - if (n <= 1) { return 1 } - return n * factorial(n - 1) -} -``` - -#### Finding Maximum in List - -**Imperative style:** -```aether,fragment -max_loop(nums, len) { - if (len == 0) { return 0 } - max_val = nums[0] - for (i = 1; i < len; i = i + 1) { - if (nums[i] > max_val) { - max_val = nums[i] - } - } - return max_val -} -``` - -**Functional style:** -```aether -import std.string - -// Recurse over the tail, then keep whichever of the head or the -// best-of-the-tail is longer. -longest(words: *StringSeq) { - match words { - [] -> { return "" } - [h | t] -> { - best_tail = longest(t) - if (string.length(h) >= string.length(best_tail)) { return h } - return best_tail - } - } - return "" -} - -main() { - words = string.seq_empty() - words = string.seq_cons("hi", words) - words = string.seq_cons("elephant", words) - words = string.seq_cons("cat", words) - println(longest(words)) // elephant -} -``` - -### When to Use Each Style - -**Use imperative style when:** -- Performance is critical (loops are often faster) -- The algorithm naturally involves mutation -- Working with fixed-size arrays by index - -**Use functional style when:** -- Processing linked data structures -- The logic follows recursive definitions (trees, etc.) -- You want self-documenting code - -### Erlang Heritage - -Aether's pattern matching in receive blocks is inspired by Erlang: - -```aether,fragment -actor Calculator { - receive { - Add(x, y) -> { - print("Sum: ") - print(x + y) - print("\n") - } - Multiply(a, b) -> { - print("Product: ") - print(a * b) - print("\n") - } - } -} -``` - -This is similar to Erlang's pattern matching in receive statements: -```erlang -receive - {add, X, Y} -> X + Y; - {multiply, A, B} -> A * B -end. -``` - -### Example 4: Prime Number Check - -```aether -is_prime(n: int) -> bool { - if (n < 2) { - return false - } - - for (i = 2; i < n; i = i + 1) { - if (n / i * i == n) { - return false // divisible, not prime - } - } - - return true -} - -main() { - numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - - for (i = 0; i < 10; i = i + 1) { - num = numbers[i] - if (is_prime(num)) { - print(num) - print(" is prime\n") - } - } -} -``` - -## Exercises - -### Exercise 1: Absolute Value - -Write a function that returns the absolute value of a number: - -```aether,fragment -abs(n) { - // Your code here -} - -main() { - print(abs(-5)) // Should print 5 - print("\n") - print(abs(3)) // Should print 3 - print("\n") -} -``` - -
-Solution - -```aether,fragment -abs(n) { - if (n < 0) { - return -n - } - return n -} -``` -
- -### Exercise 2: Power Function - -Implement x^n (x to the power of n): - -```aether,fragment -power(x, n) { - // Your code here -} - -main() { - print(power(2, 3)) // Should print 8 - print("\n") - print(power(5, 2)) // Should print 25 - print("\n") -} -``` - -
-Solution - -```aether,fragment -power(x, n) { - result = 1 - for (i = 0; i < n; i = i + 1) { - result = result * x - } - return result -} -``` -
- -### Exercise 3: FizzBuzz - -Classic programming challenge: -- Print numbers 1 to 20 -- For multiples of 3, print "Fizz" -- For multiples of 5, print "Buzz" -- For multiples of both, print "FizzBuzz" - -
-Solution - -```aether -main() { - for (i = 1; i <= 20; i = i + 1) { - if (i / 15 * 15 == i) { - print("FizzBuzz\n") - } else { - if (i / 3 * 3 == i) { - print("Fizz\n") - } else { - if (i / 5 * 5 == i) { - print("Buzz\n") - } else { - print(i) - print("\n") - } - } - } - } -} -``` -
- -## Best Practices - -### 1. Keep Functions Small - -```aether,fragment -// Good: One clear purpose -calculate_area(width, height) { - return width * height -} - -// Bad: Doing too much -process_everything(a, b, c, d, e) { - // ... 100 lines of code -} -``` - -### 2. Use Meaningful Names - -```aether,fragment -// Good -calculate_average(numbers) { } -is_valid_email(email) { } - -// Bad -calc(n) { } -check(e) { } -``` - -### 3. Avoid Deep Nesting - -```aether,fragment -// Bad: Too nested -if (a) { - if (b) { - if (c) { - if (d) { - // ... - } - } - } -} - -// Good: Early returns -process(a, b, c, d) { - if (!a) { return 0 } - if (!b) { return 0 } - if (!c) { return 0 } - if (!d) { return 0 } - - // Main logic here -} -``` - -## Next Steps - -You've learned: -- How to write functions -- Type inference for functions -- If/else conditionals -- While and for loops -- Recursive functions - -**Next Tutorial:** [Actor Concurrency](03-actor-concurrency.md) - -## Quick Reference - -```aether,fragment -// Function -add(a, b) { - return a + b -} - -// If statement -if (condition) { - // code -} else { - // code -} - -// While loop -while (condition) { - // code -} - -// For loop -for (i = 0; i < 10; i = i + 1) { - // code -} - -// Comparisons -== != < > <= >= -``` - -Happy coding! - diff --git a/docs/tutorials/03-actor-concurrency.md b/docs/tutorials/03-actor-concurrency.md deleted file mode 100644 index e50c70d8f..000000000 --- a/docs/tutorials/03-actor-concurrency.md +++ /dev/null @@ -1,616 +0,0 @@ -# Tutorial 3: Actor-Based Concurrency - -**Time:** 2 hours -**Prerequisites:** Tutorials 1 & 2 -**Goal:** Learn to write concurrent programs with actors - -## What Are Actors? - -Actors are independent units of computation that: -- Have their own **state** (private data) -- Process **messages** one at a time -- Communicate by **sending messages** (not sharing memory) -- Run **concurrently** without manual thread management - -Think of actors like people sending letters to each other - they don't share notebooks, they send messages! - -## Your First Actor - -### Simple Counter Actor - -```aether -message Increment {} - -actor Counter { - state count = 0 - - receive { - Increment() -> { - count = count + 1 - } - } -} - -main() { - // Create a counter - counter = spawn(Counter()) - - // Send it messages - counter ! Increment {} - counter ! Increment {} - counter ! Increment {} - - print("Counter processed 3 messages!\n") -} -``` - -**What's happening:** -1. `message Increment {}` defines a message type -2. `spawn(Counter())` creates a new counter actor instance -3. `counter ! Increment {}` sends an Increment message to the counter -4. The `receive` block pattern-matches incoming messages -5. Each Increment message increments the count - -## Actor Components - -### 1. State Variables - -State persists across messages: - -```aether,fragment -message Deposit { amount: int } -message Withdraw { amount: int } - -actor BankAccount { - state balance = 1000 - state account_number = 12345 - - receive { - Deposit(amount) -> { - balance = balance + amount - } - Withdraw(amount) -> { - balance = balance - amount - } - } -} -``` - -### 2. Message Definitions - -Messages are defined with the `message` keyword and can carry typed fields: - -```aether,fragment -// Empty message (no data) -message Reset {} - -// Message with one field -message SetValue { value: int } - -// Message with multiple fields -message Transfer { amount: int } -``` - -Messages are matched in the `receive` block using pattern syntax: - -```aether -message Add { value: int } -message Multiply { value: int } -message Reset {} - -actor Processor { - state total = 0 - - receive { - Add(value) -> { - total = total + value - } - Multiply(value) -> { - total = total * value - } - Reset() -> { - total = 0 - } - } -} - -main() { - proc = spawn(Processor()) - - proc ! Add { value: 10 } // Add 10 - proc ! Add { value: 5 } // Add 5 - proc ! Multiply { value: 2 } // Multiply by 2 -} -``` - -### 3. Message Processing - -Messages are processed **one at a time** in the order received: - -```aether -message LogEvent { level: int } - -actor Logger { - state message_count = 0 - - receive { - LogEvent(level) -> { - message_count = message_count + 1 - print("Message #") - print(message_count) - print(": level=") - print(level) - print("\n") - } - } -} - -main() { - logger = spawn(Logger()) - - // Send 3 messages - logger ! LogEvent { level: 1 } - logger ! LogEvent { level: 2 } - logger ! LogEvent { level: 3 } -} -``` - -## Practical Examples - -### Example 1: Shopping Cart - -```aether -message AddItem { price: int } -message RemoveItem { price: int } -message ClearCart {} - -actor ShoppingCart { - state item_count = 0 - state total_price = 0 - - receive { - AddItem(price) -> { - item_count = item_count + 1 - total_price = total_price + price - } - RemoveItem(price) -> { - if (item_count > 0) { - item_count = item_count - 1 - total_price = total_price - price - } - } - ClearCart() -> { - item_count = 0 - total_price = 0 - } - } -} - -main() { - cart = spawn(ShoppingCart()) - - // Add items - cart ! AddItem { price: 100 } // $100 item - cart ! AddItem { price: 50 } // $50 item - cart ! AddItem { price: 75 } // $75 item - - print("Shopping cart ready!\n") -} -``` - -### Example 2: Traffic Light - -```aether -message ChangeLight {} -message Tick {} - -actor TrafficLight { - state current_light = 1 // 1=red, 2=yellow, 3=green - state duration = 0 - - receive { - ChangeLight() -> { - if (current_light == 1) { - current_light = 3 // red -> green - } else { - if (current_light == 3) { - current_light = 2 // green -> yellow - } else { - current_light = 1 // yellow -> red - } - } - } - Tick() -> { - duration = duration + 1 - } - } -} - -main() { - light = spawn(TrafficLight()) - - // Simulate time passing - light ! Tick {} - light ! ChangeLight {} - light ! Tick {} - light ! ChangeLight {} -} -``` - -### Example 3: Multiple Actors - -```aether -message Increment {} - -actor Counter { - state count = 0 - - receive { - Increment() -> { - count = count + 1 - } - } -} - -main() { - // Create multiple counters - counter1 = spawn(Counter()) - counter2 = spawn(Counter()) - counter3 = spawn(Counter()) - - // Send messages to different counters - counter1 ! Increment {} - counter1 ! Increment {} - counter2 ! Increment {} - counter3 ! Increment {} - counter3 ! Increment {} - counter3 ! Increment {} - - print("Multiple actors running!\n") -} -``` - -## Actor Design Patterns - -### Pattern 1: Request-Response - -```aether,fragment -message AddTen { value: int } -message Double { value: int } - -actor Calculator { - state last_result = 0 - - receive { - AddTen(value) -> { - last_result = value + 10 - } - Double(value) -> { - last_result = value * 2 - } - } -} -``` - -### Pattern 2: State Machine - -```aether,fragment -message TakeDamage { amount: int } -message Heal { amount: int } -message LevelUp {} - -actor GamePlayer { - state health = 100 - state alive = 1 - state level = 1 - - receive { - TakeDamage(amount) -> { - if (alive == 1) { - health = health - amount - if (health <= 0) { - alive = 0 - } - } - } - Heal(amount) -> { - if (alive == 1) { - health = health + amount - if (health > 100) { - health = 100 - } - } - } - LevelUp() -> { - if (alive == 1) { - level = level + 1 - health = 100 - } - } - } -} -``` - -### Pattern 3: Accumulator - -```aether,fragment -message AddValue { value: int } - -actor Statistics { - state total = 0 - state count = 0 - state max = 0 - - receive { - AddValue(value) -> { - total = total + value - count = count + 1 - - if (value > max) { - max = value - } - } - } -} -``` - -## Exercises - -### Exercise 1: Temperature Sensor - -Create an actor that tracks temperature readings: - -```aether,fragment -message UpdateTemp { temp: int } -message Reading {} - -actor TempSensor { - state current_temp = 20 - state readings = 0 - - receive { - UpdateTemp(temp) -> { - current_temp = temp - readings = readings + 1 - } - Reading() -> { - // Print current temperature - } - } -} -``` - -### Exercise 2: Inventory Manager - -Create an actor for game inventory: - -```aether,fragment -message AddGold { amount: int } -message SpendGold { amount: int } -message AddItem {} -message RemoveItem {} - -actor Inventory { - state gold = 0 - state items = 0 - - receive { - AddGold(amount) -> { - gold = gold + amount - } - SpendGold(amount) -> { - gold = gold - amount - } - AddItem() -> { - items = items + 1 - } - RemoveItem() -> { - items = items - 1 - } - } -} -``` - -### Exercise 3: Score Tracker - -Create an actor that tracks game scores: - -```aether,fragment -message AddPoints { points: int } -message SetMultiplier { value: int } -message ResetScore {} - -actor ScoreTracker { - state score = 0 - state multiplier = 1 - state high_score = 0 - - receive { - AddPoints(points) -> { - score = score + points * multiplier - if (score > high_score) { - high_score = score - } - } - SetMultiplier(value) -> { - multiplier = value - } - ResetScore() -> { - score = 0 - multiplier = 1 - } - } -} -``` - -## Why Use Actors? - -### Benefits - -1. **No Race Conditions** - State is private, no shared memory -2. **Easy Concurrency** - No manual locks or mutexes -3. **Scalable** - Each actor is independent -4. **Isolated Failures** - A panicking actor takes down its own mailbox, not its callers' state - -### When to Use Actors - -- Independent concurrent tasks -- State machines -- Event processing -- Game entities (players, NPCs, items) -- Microservices patterns - -### When NOT to Use Actors - -- Simple sequential code -- High-speed data sharing -- Tight coupling between components - -## Best Practices - -### 1. Keep State Small - -```aether,fragment -// Good: Minimal state -actor Counter { - state count = 0 -} - -// Avoid: Too much state -actor Everything { - state count = 0 - state name = "" - state data1 = 0 - state data2 = 0 - // ... 20 more fields -} -``` - -### 2. Use Clear Message Names - -```aether,fragment -// Define descriptive message types -message Increment {} -message Decrement {} -message Reset {} - -actor Counter { - state count = 0 - - receive { - Increment() -> { - count = count + 1 - } - Decrement() -> { - count = count - 1 - } - Reset() -> { - count = 0 - } - } -} -``` - -### 3. Send and Forget - -The scheduler automatically delivers and processes messages: - -```aether,fragment -// Send; the scheduler delivers, with no manual stepping -counter ! Increment {} -counter ! Increment {} -counter ! Increment {} -``` - -## Waiting for Completion - -When you need to wait for actors to finish processing before continuing, use `wait_for_idle()`: - -```aether -message Compute { value: int } - -actor Worker { - state result = 0 - - receive { - Compute(value) -> { - result = value * 2 - } - } -} - -main() { - worker = spawn(Worker()) - - // Send work - worker ! Compute { value: 21 } - - // Wait for all messages to be processed - wait_for_idle() - - // Now safe to read state - print("Result: ") - print(worker.result) - print("\n") -} -``` - -The `wait_for_idle()` function blocks until all actors have finished processing their queued messages. This is useful when you need to: - -- Read actor state after computation -- Coordinate multiple actors before continuing -- Ensure all work completes before program exit - -For timed delays, use `sleep(milliseconds)`: - -```aether,fragment -main() { - worker = spawn(Worker()) - worker ! Start {} - - // Wait for 1 second - sleep(1000) - - worker ! Stop {} -} -``` - -## Next Steps - -You've learned: -- What actors are -- Actor state and messages -- Spawning and messaging -- Multiple concurrent actors -- Actor design patterns - -**Next:** See the [Language Reference](../language-reference.md) for complete syntax details. - -## Quick Reference - -```aether,fragment -// Define messages -message MyMessage { value: int } -message SimpleMessage {} - -// Define actor -actor MyActor { - state value = 0 - - receive { - MyMessage(value) -> { - // Handle message with data - } - SimpleMessage() -> { - // Handle empty message - } - } -} - -// Use actor -a = spawn(MyActor()) -a ! MyMessage { value: 42 } -a ! SimpleMessage {} -``` - -Happy concurrent programming! diff --git a/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md b/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md new file mode 100644 index 000000000..2bbf3ac40 --- /dev/null +++ b/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md @@ -0,0 +1,10 @@ +- **One way to learn Aether (#3475).** Getting started, the tutorial, + three more tutorials and the examples taught the same things four times, + in four orders, partly in an older style (the deprecated `export` + keyword, answers built from `print` chains). Now each has one job: + `docs/getting-started.md` installs and starts a project, the examples + are the path, `docs/tutorial.md` is that path told in prose (a chapter + per part, common mistakes, exercises with answers, every program checked + by `make check-docs`), and `docs/tutorials/` is folded into it. The + README's documentation list starts with the path. The package-publishing + details moved from getting-started to `docs/package-fetching.md`. diff --git a/tests/regression/test_actor_pair_fifo.ae b/tests/regression/test_actor_pair_fifo.ae index 7445f5050..92f3d2579 100644 --- a/tests/regression/test_actor_pair_fifo.ae +++ b/tests/regression/test_actor_pair_fifo.ae @@ -1,6 +1,6 @@ // #3090: the messages one actor sends another arrive in the order it sent -// them (tutorial.md: "Message ordering is guaranteed within a single -// actor"), while the target moves between cores. +// them (tutorial.md: "messages from one sender reach an actor in the +// order they were sent"), while the target moves between cores. // // Eight senders, spread over the cores, each stream numbered messages to // four targets at once. A target busy with messages when a send from From ee6a908c9733747631afad72e0b3157b62d4924a Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 15:58:27 -0300 Subject: [PATCH 04/13] docs: a glossary for learners (#1604) #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. --- docs/glossary.md | 182 ++++++++++++++++++ .../20261030T130200Z-added-1604-glossary.md | 4 + 2 files changed, 186 insertions(+) create mode 100644 docs/glossary.md create mode 100644 new_changelogs/20261030T130200Z-added-1604-glossary.md diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 000000000..0117656cd --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,182 @@ +# Glossary + +The words the [tutorial](tutorial.md), [the examples](../examples/README.md) +and the [language reference](language-reference.md) use, each in a few +lines, with where to read more. + +**actor.** State that only messages reach. It handles its messages one at a +time, so its state needs no lock. Declared with `actor Name { state ... +receive { ... } }`, started with `spawn`. +[Actors](language-reference.md#actors) + +**actor_ref.** What `spawn` returns: a reference to an actor, a value that +goes into variables, messages and state. `actor_ref[Member]` is a reference +to an actor of one kind. + +**ask (`?`).** `ref ? Msg { ... }` sends a message and waits for the +handler's `reply`. It ends at once with an error when the actor is dead or +its mailbox is full, and after 5 seconds when no reply comes. +[Ask pattern](language-reference.md#ask-pattern-request-reply) + +**audit trail.** The record of every check the sandbox made, allowed or +refused, which `std.audit` reads back. + +**borrowed.** How a function holds its parameters: for the call only. It +may read them and pass them on, not free them; the caller still owns them. +[Memory management](memory-management.md) + +**bounded mailbox.** An actor declared `@mailbox(bound: N)` holds at most N +messages it has not handled yet; past that a send is refused at once. +[A bounded mailbox](language-reference.md#a-bounded-mailbox) + +**builder, trailing block.** A call followed by a block on the same line +passes the block as its last argument, and the calls inside it configure +what the call builds: `sandbox.new("app") { grant_fs_read("/tmp/*") }`. +[Closures and builder DSLs](closures-and-builder-dsl.md) + +**closure.** A function value, `|x| -> x * 2`, that sees the variables +around it where it was made. + +**contract.** `requires` (what a function needs on entry) and `ensures` +(what it promises about `result`). A broken one is a panic naming the +clause. [Contracts](language-reference.md#function-contracts-requires--ensures) + +**cooperative scheduler.** The scheduler for targets without threads +(WebAssembly, ESP32): every actor runs on one thread, stepped in turn. + +**core.** One of the scheduler's threads, one per CPU by default. Each runs +the actors assigned to it. + +**counted value.** A string, closure, or a struct, array, tuple or optional +made from them. Every place that holds one owns one reference, and the +compiler releases it; the program writes no free. +[Memory management](memory-management.md) + +**credit.** In `std.stream`, the right to send one more chunk. The consumer +gives one back for each chunk it handles, so at most a window of chunks is +in flight. [std.stream](../std/stream/README.md) + +**defer.** `defer stmt` runs `stmt` when the enclosing block ends, however +it ends. [Defer](language-reference.md#defer-statement) + +**Down.** The message a monitor gets when the actor it watches ends: +`Down { ref, reason }`. The reason is "normal" for a stop, "killed" for a +kill, or the panic's message. +[Death notices](actor-concurrency.md#death-notices) + +**enum.** A set of named integer constants, written `Enum.Member`. +[Enums](language-reference.md#enums) + +**execution account.** A memory budget and a deadline a host gives one +script, across all its calls. [Execution accounts](emit-lib.md#execution-accounts-2954) + +**execution frame.** What every exported function runs in: however a call +ends (a panic, a deadline, a cancel, memory refused), the host gets +control back with an outcome. [Per-call resource caps](emit-lib.md#per-call-resource-caps) + +**export.** A name a module lets other files use, listed in its +`exports(...)` line. Also: a function a library built with `--emit=lib` +offers to C, as `aether_`. [Modules](language-reference.md#modules-and-imports) + +**extern.** A declaration of a C function the program calls: +`extern puts(s: string) -> int`. [C interoperability](c-interop.md) + +**grant.** One thing a sandbox allows: a file pattern to read or write, an +environment variable, a program, a host. A sandbox with no grants allows +nothing. [The containment sandbox](containment-sandbox.md) + +**handle.** A `ptr` a module gave the program: a list, a map, a JSON +document, a socket, a buffer from `make`. The program frees it, through its +module, usually with `defer` next to the call that made it. + +**handler.** One arm of an actor's `receive` block: what it does with one +kind of message. + +**interpolation.** `"${expr}"`: a value written into a string. + +**link.** A tie between two actors: when one ends, the other ends too, +unless it traps exits. [Links](actor-concurrency.md#links) + +**mailbox.** Where an actor's messages wait until it handles them. + +**match.** An expression that runs the first of its arms that fits the +value: literals, ranges, strings, list and struct patterns, variants. +[Match expressions](language-reference.md#match-expressions) + +**message.** A record an actor can be sent, declared with +`message Name { field: type }`. It owns the strings and arrays in it. + +**module.** A file of functions, types and constants other files import: +`std.string` from the standard library, or `lib//module.ae` of a +project. [Modules](language-reference.md#modules-and-imports) + +**monitor.** `actors.monitor(ref)` asks for one `Down` when `ref` ends. + +**optional (`T?`).** A `T` or `none`. `??` gives a fallback. +[Optionals](language-reference.md#optionals) + +**outcome.** How a call into an exported function ended, from +`aether_last_outcome()`: ok, panicked, deadline, cancelled, memory cap. + +**panic.** What a bug raises: it unwinds to the nearest `try`, and in an +actor's handler it ends that actor. Outside both, it ends the program. + +**receive.** The block of an actor that lists its handlers. + +**reply.** What a handler answers an ask with. + +**result (`T!`).** A value and an error string, `""` when it worked. `or` +gives a fallback, and `!` passes the error up. +[Result types](language-reference.md#the-t-result-type-and-or---sugar) + +**runtime.** The C library every program links: the scheduler, the actors, +the panic barrier, the sandbox. + +**safe point.** A place where a library's code checks whether its call +should end (a deadline, a cancel): every loop head, under `--emit=lib`. + +**sandbox.** A list of grants that `sandbox.enforce` holds a block of code +to. [The containment sandbox](containment-sandbox.md) + +**scheduler.** The part of the runtime that runs actors on the cores and +delivers their messages. + +**self.** Inside a handler, the actor's own `actor_ref`. + +**send (`!`).** `ref ! Msg { ... }`: a message on its way, without waiting. + +**slice (`T[]`).** A view of elements stored elsewhere: a pointer and a +length. `xs[lo..hi]` makes one. [Slices](language-reference.md#slices-t) + +**spawn.** `spawn(Name())` starts an actor and returns its `actor_ref`. + +**state.** The fields an actor keeps between messages, declared with +`state name = value`. [State variables](language-reference.md#state-variables) + +**std, contrib.** `std/` is the standard library every install has; +`contrib/` holds modules that need a library or a toolchain of their own +(a database, another language). + +**step.** One run of an actor's handler for one message. + +**stream.** A flow of chunks from one actor to another, held to a window +of credits by `std.stream`. + +**struct.** A record of named fields, held by value: assigning it copies +it. [Structs](language-reference.md#structs) + +**sum type.** A value that is exactly one of several structs, its +variants: `type Shape = Circle | Rect`. A `match` over it must cover each. +[Sum types](language-reference.md#sum--variant-types) + +**supervisor.** An actor that starts children, links them, and restarts +one that fails under the same ref. [Supervisors](actor-concurrency.md#supervisors) + +**trap exits.** An actor that calls `actors.trap_exits(true)` gets a +`Down` when an actor linked to it ends, instead of ending too. + +**try, catch.** `try { ... } catch msg { ... }` runs the block and, when a +panic unwinds out of it, the catch with the panic's message. + +**wait_for_idle().** Waits until every actor has nothing left to do. Called +from main, never from inside an actor. diff --git a/new_changelogs/20261030T130200Z-added-1604-glossary.md b/new_changelogs/20261030T130200Z-added-1604-glossary.md new file mode 100644 index 000000000..e20764a5a --- /dev/null +++ b/new_changelogs/20261030T130200Z-added-1604-glossary.md @@ -0,0 +1,4 @@ +- **A glossary (#1604).** `docs/glossary.md` defines the words the + tutorial, the examples and the reference use (actor, handle, borrowed, + link, safe point, sum type and the rest), each with where to read more, + and the path links it. From 27bf8199b3cc0df7cab2586aee9627bb4decc47a Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 16:22:16 -0300 Subject: [PATCH 05/13] examples: a first Rosetta Code tranche, measured against Go and Python (#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. --- docs/tutorial.md | 5 ++ examples/08-rosetta/01-fizzbuzz.ae | 36 ++++++++ examples/08-rosetta/02-ackermann.ae | 30 +++++++ examples/08-rosetta/03-fibonacci.ae | 39 +++++++++ examples/08-rosetta/04-factorial.ae | 32 +++++++ examples/08-rosetta/05-gcd.ae | 27 ++++++ examples/08-rosetta/06-sieve.ae | 36 ++++++++ examples/08-rosetta/07-hanoi.ae | 37 ++++++++ examples/08-rosetta/08-n-queens.ae | 86 +++++++++++++++++++ examples/08-rosetta/09-reverse-string.ae | 30 +++++++ examples/08-rosetta/10-palindrome.ae | 46 ++++++++++ examples/08-rosetta/11-roman-encode.ae | 37 ++++++++ examples/08-rosetta/13-binary-digits.ae | 24 ++++++ examples/08-rosetta/14-quicksort.ae | 53 ++++++++++++ examples/08-rosetta/15-binary-search.ae | 39 +++++++++ examples/08-rosetta/16-hailstone.ae | 38 ++++++++ examples/08-rosetta/19-levenshtein.ae | 47 ++++++++++ examples/08-rosetta/20-happy-numbers.ae | 31 +++++++ examples/08-rosetta/21-perfect-numbers.ae | 32 +++++++ examples/08-rosetta/22-look-and-say.ae | 46 ++++++++++ examples/08-rosetta/23-josephus.ae | 44 ++++++++++ examples/08-rosetta/24-pascals-triangle.ae | 42 +++++++++ examples/08-rosetta/25-prime-decomposition.ae | 55 ++++++++++++ examples/README.md | 42 +++++++++ ...150000Z-added-1191-rosetta-code-tranche.md | 6 ++ 25 files changed, 940 insertions(+) create mode 100644 examples/08-rosetta/01-fizzbuzz.ae create mode 100644 examples/08-rosetta/02-ackermann.ae create mode 100644 examples/08-rosetta/03-fibonacci.ae create mode 100644 examples/08-rosetta/04-factorial.ae create mode 100644 examples/08-rosetta/05-gcd.ae create mode 100644 examples/08-rosetta/06-sieve.ae create mode 100644 examples/08-rosetta/07-hanoi.ae create mode 100644 examples/08-rosetta/08-n-queens.ae create mode 100644 examples/08-rosetta/09-reverse-string.ae create mode 100644 examples/08-rosetta/10-palindrome.ae create mode 100644 examples/08-rosetta/11-roman-encode.ae create mode 100644 examples/08-rosetta/13-binary-digits.ae create mode 100644 examples/08-rosetta/14-quicksort.ae create mode 100644 examples/08-rosetta/15-binary-search.ae create mode 100644 examples/08-rosetta/16-hailstone.ae create mode 100644 examples/08-rosetta/19-levenshtein.ae create mode 100644 examples/08-rosetta/20-happy-numbers.ae create mode 100644 examples/08-rosetta/21-perfect-numbers.ae create mode 100644 examples/08-rosetta/22-look-and-say.ae create mode 100644 examples/08-rosetta/23-josephus.ae create mode 100644 examples/08-rosetta/24-pascals-triangle.ae create mode 100644 examples/08-rosetta/25-prime-decomposition.ae create mode 100644 new_changelogs/20261030T150000Z-added-1191-rosetta-code-tranche.md diff --git a/docs/tutorial.md b/docs/tutorial.md index 2e8b1bb4a..52141794e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -707,6 +707,11 @@ supervisor (a process with a deadline and nothing left behind) and a terminal calculator (a builder DSL with closures). Read them for how the pieces fit, then write your own. +The path ends with [Rosetta Code tasks](../examples/README.md#8-rosetta-code): +classic problems (FizzBuzz, the N queens, quicksort, Levenshtein distance) +solved in Aether, with their length beside the Go and Python solutions. +They are good exercises too: try one before reading its file. + ## Where next - [Language reference](language-reference.md): every construct, in full. diff --git a/examples/08-rosetta/01-fizzbuzz.ae b/examples/08-rosetta/01-fizzbuzz.ae new file mode 100644 index 000000000..b90aade67 --- /dev/null +++ b/examples/08-rosetta/01-fizzbuzz.ae @@ -0,0 +1,36 @@ +// FizzBuzz: the numbers 1 to 100, with Fizz, Buzz and FizzBuzz for multiples. +// +// The task prints 1 to 100, but Fizz for a multiple of three, Buzz for a +// multiple of five and FizzBuzz for a multiple of both. A clause set with +// guards says it: the first clause whose guard holds gives the word. All +// hundred are printed, ten to a line, to keep the output short. +// +// Run: ae run examples/08-rosetta/01-fizzbuzz.ae +// See: https://rosettacode.org/wiki/FizzBuzz + +fizzbuzz(n: int) when n % 15 == 0 -> string -> "FizzBuzz" +fizzbuzz(n: int) when n % 3 == 0 -> string -> "Fizz" +fizzbuzz(n: int) when n % 5 == 0 -> string -> "Buzz" +fizzbuzz(n: int) -> string -> "${n}" + +main() { + for row in 0..10 { + line = fizzbuzz(row * 10 + 1) + for col in 2..11 { + line = "${line} ${fizzbuzz(row * 10 + col)}" + } + println(line) + } +} + +// Output: +// 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz +// 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz +// Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz +// 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz +// 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz +// Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz +// 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz +// 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz +// Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz +// 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz diff --git a/examples/08-rosetta/02-ackermann.ae b/examples/08-rosetta/02-ackermann.ae new file mode 100644 index 000000000..5ec1a1916 --- /dev/null +++ b/examples/08-rosetta/02-ackermann.ae @@ -0,0 +1,30 @@ +// Ackermann function: A(m, n), a recursion that is not primitive recursive. +// +// The task is a function for A(m, n), defined by three cases: n + 1 when +// m is 0, A(m - 1, 1) when n is 0, and A(m - 1, A(m, n - 1)) otherwise. +// Three clauses are those cases, a literal 0 matching a zero argument. +// The table is m = 0 to 3 by n = 0 to 4; it stops at m = 3 because +// A(4, 1) recurses too deep for a short demo. +// +// Run: ae run examples/08-rosetta/02-ackermann.ae +// See: https://rosettacode.org/wiki/Ackermann_function + +ackermann(0, n: int) -> int -> n + 1 +ackermann(m: int, 0) -> int -> ackermann(m - 1, 1) +ackermann(m: int, n: int) -> int -> ackermann(m - 1, ackermann(m, n - 1)) + +main() { + for m in 0..4 { + line = "A(${m}, 0..4):" + for n in 0..5 { + line = "${line} ${ackermann(m, n)}" + } + println(line) + } +} + +// Output: +// A(0, 0..4): 1 2 3 4 5 +// A(1, 0..4): 2 3 4 5 6 +// A(2, 0..4): 3 5 7 9 11 +// A(3, 0..4): 5 13 29 61 125 diff --git a/examples/08-rosetta/03-fibonacci.ae b/examples/08-rosetta/03-fibonacci.ae new file mode 100644 index 000000000..5c1dadbd6 --- /dev/null +++ b/examples/08-rosetta/03-fibonacci.ae @@ -0,0 +1,39 @@ +// Fibonacci sequence: the nth Fibonacci number, for negative n too. +// +// The task is a function for F(n), where F(0) = 0, F(1) = 1 and each +// later term is the sum of the two before; F(-n) = (-1)^(n+1) F(n) is +// optional. A clause with a guard takes a negative n to the positive one, +// and the other walks the pair (a, b) along with a tuple assignment. The +// program shows F(0) to F(15), F(-1) to F(-8), and F(92). +// +// Run: ae run examples/08-rosetta/03-fibonacci.ae +// See: https://rosettacode.org/wiki/Fibonacci_sequence + +fib(n: int) when n < 0 -> long -> if n % 2 == 0 { -fib(-n) } else { fib(-n) } +fib(n: int) -> long { + long a = 0 + long b = 1 + for i in 0..n { + a, b = (b, a + b) + } + return a +} + +main() { + line = "F(0) to F(15):" + for n in 0..16 { + line = "${line} ${fib(n)}" + } + println(line) + line = "F(-1) to F(-8):" + for n in 1..9 { + line = "${line} ${fib(-n)}" + } + println(line) + println("F(92) = ${fib(92)}, the last that fits in a long") +} + +// Output: +// F(0) to F(15): 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 +// F(-1) to F(-8): 1 -1 2 -3 5 -8 13 -21 +// F(92) = 7540113804746346429, the last that fits in a long diff --git a/examples/08-rosetta/04-factorial.ae b/examples/08-rosetta/04-factorial.ae new file mode 100644 index 000000000..396423608 --- /dev/null +++ b/examples/08-rosetta/04-factorial.ae @@ -0,0 +1,32 @@ +// Factorial: n!, the product of 1 to n, with 0! = 1. +// +// The task is a function for n!, where trapping a negative n is optional. +// Three clauses: a guard turns a negative n into an error, a literal 0 +// gives 1, and any other n recurses. The result is a `long!`, a value or +// an error, and `!` after the inner call hands an error up unchanged. +// The program shows 0!, 1!, 5!, 10! and 20!, and the error for -3. +// +// Run: ae run examples/08-rosetta/04-factorial.ae +// See: https://rosettacode.org/wiki/Factorial + +factorial(n: int) when n < 0 -> long! { + return 0, "no factorial of ${n}" +} +factorial(0) -> long! -> 1 +factorial(n: int) -> long! -> n * factorial(n - 1)! + +main() { + for n in [0, 1, 5, 10, 20] { + println("${n}! = ${factorial(n) or 0}") + } + _, err = factorial(-3) + println("-3! fails: ${err}") +} + +// Output: +// 0! = 1 +// 1! = 1 +// 5! = 120 +// 10! = 3628800 +// 20! = 2432902008176640000 +// -3! fails: no factorial of -3 diff --git a/examples/08-rosetta/05-gcd.ae b/examples/08-rosetta/05-gcd.ae new file mode 100644 index 000000000..7887cf7d6 --- /dev/null +++ b/examples/08-rosetta/05-gcd.ae @@ -0,0 +1,27 @@ +// Greatest common divisor: the largest integer dividing both of two integers. +// +// The task is the GCD of two integers. Euclid's algorithm is two clauses: +// a literal 0 in the second place ends it with the first number, made +// positive, and any other pair recurses on (b, a % b). Five pairs are +// shown, a negative number and a zero among them. +// +// Run: ae run examples/08-rosetta/05-gcd.ae +// See: https://rosettacode.org/wiki/Greatest_common_divisor + +gcd(a: int, 0) -> int -> if a < 0 { -a } else { a } +gcd(a: int, b: int) -> int -> gcd(b, a % b) + +main() { + println("gcd(40902, 24140) = ${gcd(40902, 24140)}") + println("gcd(1071, 462) = ${gcd(1071, 462)}") + println("gcd(-12, 18) = ${gcd(-12, 18)}") + println("gcd(17, 5) = ${gcd(17, 5)}") + println("gcd(0, 9) = ${gcd(0, 9)}") +} + +// Output: +// gcd(40902, 24140) = 34 +// gcd(1071, 462) = 21 +// gcd(-12, 18) = 6 +// gcd(17, 5) = 1 +// gcd(0, 9) = 9 diff --git a/examples/08-rosetta/06-sieve.ae b/examples/08-rosetta/06-sieve.ae new file mode 100644 index 000000000..8ff6981ca --- /dev/null +++ b/examples/08-rosetta/06-sieve.ae @@ -0,0 +1,36 @@ +// Sieve of Eratosthenes: the primes up to a limit, by crossing out multiples. +// +// The task is the sieve itself: cross out the multiples of each prime +// found, from its square up, and what stays uncrossed is prime. A `make` +// buffer of bools holds the marks, freed by the `defer` beside it, and the +// primes go into a slice that grows with `push`. The limit here is 100. +// +// Run: ae run examples/08-rosetta/06-sieve.ae +// See: https://rosettacode.org/wiki/Sieve_of_Eratosthenes + +primes_up_to(limit: int) -> int[] { + crossed = make([]bool, limit + 1) + defer free(crossed) + int[] primes = [] + for n in 2..limit + 1 { + if !crossed[n] { + primes.push(n) + for (m = n * n; m <= limit; m += n) { + crossed[m] = true + } + } + } + return primes +} + +main() { + primes = primes_up_to(100) + line = "${primes.len} primes up to 100:" + for p in primes { + line = "${line} ${p}" + } + println(line) +} + +// Output: +// 25 primes up to 100: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 diff --git a/examples/08-rosetta/07-hanoi.ae b/examples/08-rosetta/07-hanoi.ae new file mode 100644 index 000000000..180559226 --- /dev/null +++ b/examples/08-rosetta/07-hanoi.ae @@ -0,0 +1,37 @@ +// Towers of Hanoi: the moves that carry a stack of disks to another peg. +// +// The task is to solve the puzzle with recursion: move the n - 1 disks +// above out of the way, move disk n, and put the n - 1 back on top of it. +// Two clauses: a literal 0 is no disks and nothing to do. Four disks take +// fifteen moves. +// +// Run: ae run examples/08-rosetta/07-hanoi.ae +// See: https://rosettacode.org/wiki/Towers_of_Hanoi + +hanoi(0, src: string, dst: string, via: string) {} +hanoi(n: int, src: string, dst: string, via: string) { + hanoi(n - 1, src, via, dst) + println("move disk ${n} from ${src} to ${dst}") + hanoi(n - 1, via, dst, src) +} + +main() { + hanoi(4, "A", "C", "B") +} + +// Output: +// move disk 1 from A to B +// move disk 2 from A to C +// move disk 1 from B to C +// move disk 3 from A to B +// move disk 1 from C to A +// move disk 2 from C to B +// move disk 1 from A to B +// move disk 4 from A to C +// move disk 1 from B to C +// move disk 2 from B to A +// move disk 1 from C to A +// move disk 3 from B to C +// move disk 1 from A to B +// move disk 2 from A to C +// move disk 1 from B to C diff --git a/examples/08-rosetta/08-n-queens.ae b/examples/08-rosetta/08-n-queens.ae new file mode 100644 index 000000000..c9dcc67e0 --- /dev/null +++ b/examples/08-rosetta/08-n-queens.ae @@ -0,0 +1,86 @@ +// N-queens problem: queens on an N x N board, no two attacking each other. +// +// The task is the eight queens puzzle, extended to N x N. The solver puts +// one queen a row, in each column no queen above attacks, and backtracks; +// a slice holds the column of each row's queen. It counts the solutions +// for N = 1 to 10 (92 for eight) and draws the first 8 x 8 one. +// +// Run: ae run examples/08-rosetta/08-n-queens.ae +// See: https://rosettacode.org/wiki/N-queens_problem + +// Whether a queen at (row, col) is safe from the queens in the rows above. +safe(cols: int[], row: int, col: int) -> bool { + for r in 0..row { + d = cols[r] - col + if d == 0 || d == row - r || d == r - row { + return false + } + } + return true +} + +// Counts the boards that complete from `row` down, and copies the first +// one found into `first`. +place(cols: int[], row: int, first: int[]) -> int { + if row == cols.len { + if first[0] < 0 { + for r in 0..cols.len { + first[r] = cols[r] + } + } + return 1 + } + count = 0 + for col in 0..cols.len { + if safe(cols, row, col) { + cols[row] = col + count += place(cols, row + 1, first) + } + } + return count +} + +draw(cols: int[]) { + for r in 0..cols.len { + line = "" + for c in 0..cols.len { + square = if cols[r] == c { "Q" } else { "." } + line = "${line}${square}" + } + println(line) + } +} + +main() { + for n in 1..11 { + cols = make([]int, n) + defer free(cols) + first = make([]int, n) + defer free(first) + first[0] = -1 + println("${n} x ${n}: ${place(cols, 0, first)}") + if n == 8 { + draw(first) + } + } +} + +// Output: +// 1 x 1: 1 +// 2 x 2: 0 +// 3 x 3: 0 +// 4 x 4: 2 +// 5 x 5: 10 +// 6 x 6: 4 +// 7 x 7: 40 +// 8 x 8: 92 +// Q....... +// ....Q... +// .......Q +// .....Q.. +// ..Q..... +// ......Q. +// .Q...... +// ...Q.... +// 9 x 9: 352 +// 10 x 10: 724 diff --git a/examples/08-rosetta/09-reverse-string.ae b/examples/08-rosetta/09-reverse-string.ae new file mode 100644 index 000000000..b65050f22 --- /dev/null +++ b/examples/08-rosetta/09-reverse-string.ae @@ -0,0 +1,30 @@ +// Reverse a string: the characters of a string in the opposite order. +// +// The task turns "asdf" into "fdsa", and its extra credit keeps each +// combining character on the letter it marks. std.unicode counts and cuts +// grapheme clusters, the characters a reader sees, so each moves whole. +// The inputs are the task's two examples and "Hello, World!". +// +// Run: ae run examples/08-rosetta/09-reverse-string.ae +// See: https://rosettacode.org/wiki/Reverse_a_string + +import std.unicode + +reverse(s: string) -> string { + out = "" + for i in 0..unicode.grapheme_len(s) { + out = "${unicode.grapheme_substring(s, i, 1)}${out}" + } + return out +} + +main() { + for s in ["asdf", "as⃝df̅", "Hello, World!"] { + println("${s} -> ${reverse(s)}") + } +} + +// Output: +// asdf -> fdsa +// as⃝df̅ -> f̅ds⃝a +// Hello, World! -> !dlroW ,olleH diff --git a/examples/08-rosetta/10-palindrome.ae b/examples/08-rosetta/10-palindrome.ae new file mode 100644 index 000000000..5b63e588c --- /dev/null +++ b/examples/08-rosetta/10-palindrome.ae @@ -0,0 +1,46 @@ +// Palindrome detection: whether a phrase reads the same backward and forward. +// +// The task checks a sequence of bytes; its extra credit is an inexact +// check that ignores case, spaces and punctuation. The exact check walks +// in from both ends. The inexact one keeps the letters and digits of the +// lowercased text, picked by a `match` on byte ranges, and checks those. +// Four phrases are tried, two of them palindromes only inexactly. +// +// Run: ae run examples/08-rosetta/10-palindrome.ae +// See: https://rosettacode.org/wiki/Palindrome_detection + +import std.string + +palindrome(s: string) -> bool { + n = string.length(s) + for i in 0..n / 2 { + if s[i] != s[n - 1 - i] { + return false + } + } + return true +} + +inexact_palindrome(s: string) -> bool { + lower = string.to_lower(s) + kept = "" + for i in 0..string.length(lower) { + kept = match lower[i] { + 97..=122, 48..=57 -> "${kept}${string.from_char(lower[i])}" // a-z, 0-9 + _ -> kept + } + } + return palindrome(kept) +} + +main() { + for s in ["racecar", "rosetta", "A man, a plan, a canal: Panama!", "In girum imus nocte et consumimur igni"] { + println("\"${s}\": exact ${palindrome(s)}, inexact ${inexact_palindrome(s)}") + } +} + +// Output: +// "racecar": exact true, inexact true +// "rosetta": exact false, inexact false +// "A man, a plan, a canal: Panama!": exact false, inexact true +// "In girum imus nocte et consumimur igni": exact false, inexact true diff --git a/examples/08-rosetta/11-roman-encode.ae b/examples/08-rosetta/11-roman-encode.ae new file mode 100644 index 000000000..7a7295ab8 --- /dev/null +++ b/examples/08-rosetta/11-roman-encode.ae @@ -0,0 +1,37 @@ +// Roman numerals/Encode: a positive integer written as a Roman numeral. +// +// The task writes each decimal digit separately, from the left, skipping +// zeros: 1990 is MCMXC, 2008 MMVIII, 1666 MDCLXVI; 2026 and 3999 are shown +// too. Two constant tables pair each value with its symbol, the +// subtractive pairs (CM, XC, IV) included, so taking the largest value +// that fits, again and again, spells the numeral. +// +// Run: ae run examples/08-rosetta/11-roman-encode.ae +// See: https://rosettacode.org/wiki/Roman_numerals/Encode + +const VALUES[] = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1] +const SYMBOLS[] = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] + +roman(n: int) -> string { + out = "" + for i in 0..VALUES.len { + while n >= VALUES[i] { + out = "${out}${SYMBOLS[i]}" + n -= VALUES[i] + } + } + return out +} + +main() { + for n in [1990, 2008, 1666, 2026, 3999] { + println("${n} = ${roman(n)}") + } +} + +// Output: +// 1990 = MCMXC +// 2008 = MMVIII +// 1666 = MDCLXVI +// 2026 = MMXXVI +// 3999 = MMMCMXCIX diff --git a/examples/08-rosetta/13-binary-digits.ae b/examples/08-rosetta/13-binary-digits.ae new file mode 100644 index 000000000..b8295bcd4 --- /dev/null +++ b/examples/08-rosetta/13-binary-digits.ae @@ -0,0 +1,24 @@ +// Binary digits: a non-negative integer written in base 2. +// +// The task prints the binary digits of a number and nothing else, with no +// leading zeros: 0, 5, 50 and 9000 give 0, 101, 110010 and 10001100101000. +// Two clauses: a guard takes 0 and 1 as they are, and a larger n is the +// digits of n / 2 followed by n % 2, joined by interpolation. +// +// Run: ae run examples/08-rosetta/13-binary-digits.ae +// See: https://rosettacode.org/wiki/Binary_digits + +binary(n: int) when n < 2 -> string -> "${n}" +binary(n: int) -> string -> "${binary(n / 2)}${n % 2}" + +main() { + for n in [0, 5, 50, 9000] { + println(binary(n)) + } +} + +// Output: +// 0 +// 101 +// 110010 +// 10001100101000 diff --git a/examples/08-rosetta/14-quicksort.ae b/examples/08-rosetta/14-quicksort.ae new file mode 100644 index 000000000..2e8b7db23 --- /dev/null +++ b/examples/08-rosetta/14-quicksort.ae @@ -0,0 +1,53 @@ +// Sorting algorithms/Quicksort: an array sorted by partitioning around a pivot. +// +// The task sorts an array with quicksort; this is its in-place version, +// over nine integers. The partition swaps around the middle element, and +// the two sides are sorted as sub-slices: `xs[..right + 1]` and +// `xs[left..]` view the same elements, so the recursive calls sort them +// where they are. A `match` on list patterns prints the result. +// +// Run: ae run examples/08-rosetta/14-quicksort.ae +// See: https://rosettacode.org/wiki/Sorting_algorithms/Quicksort + +quicksort(xs: int[]) { + if xs.len < 2 { + return + } + pivot = xs[xs.len / 2] + left = 0 + right = xs.len - 1 + while left <= right { + while xs[left] < pivot { + left += 1 + } + while xs[right] > pivot { + right -= 1 + } + if left <= right { + xs[left], xs[right] = (xs[right], xs[left]) + left += 1 + right -= 1 + } + } + quicksort(xs[..right + 1]) + quicksort(xs[left..]) +} + +show(xs: int[]) -> string { + return match xs { + [] -> "" + [x] -> "${x}" + [x | rest] -> "${x} ${show(rest)}" + } +} + +main() { + xs = [65, 2, -31, 0, 99, 2, 83, 782, 1] + println("before: ${show(xs)}") + quicksort(xs) + println("after: ${show(xs)}") +} + +// Output: +// before: 65 2 -31 0 99 2 83 782 1 +// after: -31 0 1 2 2 65 83 99 782 diff --git a/examples/08-rosetta/15-binary-search.ae b/examples/08-rosetta/15-binary-search.ae new file mode 100644 index 000000000..86053e6be --- /dev/null +++ b/examples/08-rosetta/15-binary-search.ae @@ -0,0 +1,39 @@ +// Binary search: where a value is in a sorted array, or that it is not there. +// +// The task searches a sorted integer array by halving the range, and says +// whether the value was found and at which index; here, five values in the +// first ten primes. The answer is an `int?`: a clause with a guard gives +// `none` once the range is empty, and the caller's `match` takes `some(i)` +// and `none` apart. The midpoint low + (high - low) / 2 cannot overflow. +// +// Run: ae run examples/08-rosetta/15-binary-search.ae +// See: https://rosettacode.org/wiki/Binary_search + +search(xs: int[], value: int, low: int, high: int) when high < low -> int? -> none +search(xs: int[], value: int, low: int, high: int) -> int? { + mid = low + (high - low) / 2 + if xs[mid] > value { + return search(xs, value, low, mid - 1) + } + if xs[mid] < value { + return search(xs, value, mid + 1, high) + } + return mid +} + +main() { + xs = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + for value in [2, 13, 29, 4, 30] { + match search(xs, value, 0, xs.len - 1) { + some(i) -> println("${value} is at index ${i}") + none -> println("${value} is not in the array") + } + } +} + +// Output: +// 2 is at index 0 +// 13 is at index 5 +// 29 is at index 9 +// 4 is not in the array +// 30 is not in the array diff --git a/examples/08-rosetta/16-hailstone.ae b/examples/08-rosetta/16-hailstone.ae new file mode 100644 index 000000000..d0e6e92a2 --- /dev/null +++ b/examples/08-rosetta/16-hailstone.ae @@ -0,0 +1,38 @@ +// Hailstone sequence: the Collatz sequence from n down to 1. +// +// The task: generate the sequence (n / 2 after an even n, 3n + 1 after an +// odd one, until 1), show that 27's has 112 elements, and find the start +// below 100,000 with the longest one. The sequence is a `long[]` that +// grows with `push`; each is released when its local is bound again. +// +// Run: ae run examples/08-rosetta/16-hailstone.ae +// See: https://rosettacode.org/wiki/Hailstone_sequence + +hailstone(n: long) -> long[] { + long[] seq = [] + seq.push(n) + while n != 1 { + n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 } + seq.push(n) + } + return seq +} + +main() { + h = hailstone(27) + println("27: ${h.len} elements, ${h[0]}, ${h[1]}, ${h[2]}, ${h[3]}, ..., ${h[h.len - 4]}, ${h[h.len - 3]}, ${h[h.len - 2]}, ${h[h.len - 1]}") + best = 1 + longest = 1 + for n in 1..100000 { + seq = hailstone(n) + if seq.len > longest { + best = n + longest = seq.len + } + } + println("below 100000: ${best}, with ${longest} elements") +} + +// Output: +// 27: 112 elements, 27, 82, 41, 124, ..., 8, 4, 2, 1 +// below 100000: 77031, with 351 elements diff --git a/examples/08-rosetta/19-levenshtein.ae b/examples/08-rosetta/19-levenshtein.ae new file mode 100644 index 000000000..ed33d667a --- /dev/null +++ b/examples/08-rosetta/19-levenshtein.ae @@ -0,0 +1,47 @@ +// Levenshtein distance: the fewest one-character edits between two strings. +// +// The task shows the distance from "kitten" to "sitting", which is 3; the +// task's "rosettacode" to "raisethysword" (8), the same reversed, and +// "saturday" to "sunday" are shown too. The table is kept one row at a +// time, in a `make` buffer freed by the `defer` beside it: each cell is the +// cheapest of a deletion, an insertion and a substitution, free when the +// two characters agree. +// +// Run: ae run examples/08-rosetta/19-levenshtein.ae +// See: https://rosettacode.org/wiki/Levenshtein_distance + +import std.math +import std.string + +levenshtein(a: string, b: string) -> int { + n = string.length(b) + row = make([]int, n + 1) + defer free(row) + for j in 0..n + 1 { + row[j] = j + } + for i in 1..string.length(a) + 1 { + diagonal = row[0] + row[0] = i + for j in 1..n + 1 { + above = row[j] + cost = if a[i - 1] == b[j - 1] { 0 } else { 1 } + row[j] = math.min_int(math.min_int(above, row[j - 1]) + 1, diagonal + cost) + diagonal = above + } + } + return row[n] +} + +main() { + println("kitten -> sitting: ${levenshtein("kitten", "sitting")}") + println("rosettacode -> raisethysword: ${levenshtein("rosettacode", "raisethysword")}") + println("edocattesor -> drowsyhtesiar: ${levenshtein("edocattesor", "drowsyhtesiar")}") + println("saturday -> sunday: ${levenshtein("saturday", "sunday")}") +} + +// Output: +// kitten -> sitting: 3 +// rosettacode -> raisethysword: 8 +// edocattesor -> drowsyhtesiar: 8 +// saturday -> sunday: 3 diff --git a/examples/08-rosetta/20-happy-numbers.ae b/examples/08-rosetta/20-happy-numbers.ae new file mode 100644 index 000000000..e72a131fd --- /dev/null +++ b/examples/08-rosetta/20-happy-numbers.ae @@ -0,0 +1,31 @@ +// Happy numbers: numbers whose digit-square sums reach 1. +// +// The task prints the first 8 happy numbers. Replace n by the sum of the +// squares of its digits until it reaches 1, happy, or loops forever. Every +// unhappy number falls into the one loop through 4, so two clauses with +// literals end the walk and a third takes the next step. +// +// Run: ae run examples/08-rosetta/20-happy-numbers.ae +// See: https://rosettacode.org/wiki/Happy_numbers + +digit_squares(0) -> int -> 0 +digit_squares(n: int) -> int -> (n % 10) * (n % 10) + digit_squares(n / 10) + +happy(1) -> bool -> true +happy(4) -> bool -> false +happy(n: int) -> bool -> happy(digit_squares(n)) + +main() { + line = "the first 8:" + found = 0 + for (n = 1; found < 8; n += 1) { + if happy(n) { + line = "${line} ${n}" + found += 1 + } + } + println(line) +} + +// Output: +// the first 8: 1 7 10 13 19 23 28 31 diff --git a/examples/08-rosetta/21-perfect-numbers.ae b/examples/08-rosetta/21-perfect-numbers.ae new file mode 100644 index 000000000..2ee0f627d --- /dev/null +++ b/examples/08-rosetta/21-perfect-numbers.ae @@ -0,0 +1,32 @@ +// Perfect numbers: numbers equal to the sum of their proper divisors. +// +// The task is a function that says whether a number is perfect, as 6 is: +// 1 + 2 + 3 = 6. It adds up the divisors to half the number, the largest +// a proper divisor can be. The program lists the perfect numbers below +// 10,000. +// +// Run: ae run examples/08-rosetta/21-perfect-numbers.ae +// See: https://rosettacode.org/wiki/Perfect_numbers + +perfect(n: int) -> bool { + total = 0 + for d in 1..n / 2 + 1 { + if n % d == 0 { + total += d + } + } + return total == n +} + +main() { + line = "below 10000:" + for n in 1..10000 { + if perfect(n) { + line = "${line} ${n}" + } + } + println(line) +} + +// Output: +// below 10000: 6 28 496 8128 diff --git a/examples/08-rosetta/22-look-and-say.ae b/examples/08-rosetta/22-look-and-say.ae new file mode 100644 index 000000000..4cd98ddf8 --- /dev/null +++ b/examples/08-rosetta/22-look-and-say.ae @@ -0,0 +1,46 @@ +// Look-and-say sequence: each term says the runs of digits in the one before. +// +// The task generates successive terms: 1 is "one 1", 11, which is "two +// 1s", 21, then 1211, and so on. Each run of one digit becomes its length +// followed by the digit, by interpolation. The program prints the first 10 +// terms. +// +// Run: ae run examples/08-rosetta/22-look-and-say.ae +// See: https://rosettacode.org/wiki/Look-and-say_sequence + +import std.string + +next_term(term: string) -> string { + out = "" + n = string.length(term) + i = 0 + while i < n { + j = i + while j < n && term[j] == term[i] { + j += 1 + } + out = "${out}${j - i}${string.substring(term, i, i + 1)}" + i = j + } + return out +} + +main() { + term = "1" + for k in 0..10 { + println(term) + term = next_term(term) + } +} + +// Output: +// 1 +// 11 +// 21 +// 1211 +// 111221 +// 312211 +// 13112221 +// 1113213211 +// 31131211131221 +// 13211311123113112211 diff --git a/examples/08-rosetta/23-josephus.ae b/examples/08-rosetta/23-josephus.ae new file mode 100644 index 000000000..db9daaeba --- /dev/null +++ b/examples/08-rosetta/23-josephus.ae @@ -0,0 +1,44 @@ +// Josephus problem: who survives when every kth prisoner in a circle is killed. +// +// The task numbers the prisoners 0 to n - 1 and asks who is left; with +// n = 41 and k = 3, Josephus stood at 30. Two clauses give the survivor by +// recurrence. The extra credit, the whole killing sequence, walks the +// circle as the executioner does, over a `make` buffer of who is dead. +// +// Run: ae run examples/08-rosetta/23-josephus.ae +// See: https://rosettacode.org/wiki/Josephus_problem + +survivor(1, k: int) -> int -> 0 +survivor(n: int, k: int) -> int -> (survivor(n - 1, k) + k) % n + +killing_order(n: int, k: int) -> int[] { + dead = make([]bool, n) + defer free(dead) + int[] order = [] + at = n - 1 + while order.len < n { + living = 0 + while living < k { + at = (at + 1) % n + if !dead[at] { + living += 1 + } + } + dead[at] = true + order.push(at) + } + return order +} + +main() { + println("n = 41, k = 3: survivor ${survivor(41, 3)}") + order = killing_order(5, 2) + println("n = 5, k = 2: killed ${order[0]} ${order[1]} ${order[2]} ${order[3]}, survivor ${order[4]}") + order = killing_order(41, 3) + println("n = 41, k = 3: the last three ${order[38]} ${order[39]} ${order[40]}") +} + +// Output: +// n = 41, k = 3: survivor 30 +// n = 5, k = 2: killed 1 3 0 4, survivor 2 +// n = 41, k = 3: the last three 34 15 30 diff --git a/examples/08-rosetta/24-pascals-triangle.ae b/examples/08-rosetta/24-pascals-triangle.ae new file mode 100644 index 000000000..fdcd2e8eb --- /dev/null +++ b/examples/08-rosetta/24-pascals-triangle.ae @@ -0,0 +1,42 @@ +// Pascal's triangle: its first n rows, each entry the sum of the two above it. +// +// The task prints the first n rows, f(1) being the single row 1; for +// n <= 0 this prints nothing, and here n is 8. One row is kept and updated +// in place from the right, each entry plus the one to its left, then grown +// by a 1 with `push`. A `match` on list patterns prints a row. +// +// Run: ae run examples/08-rosetta/24-pascals-triangle.ae +// See: https://rosettacode.org/wiki/Pascal's_triangle + +show(xs: int[]) -> string { + return match xs { + [] -> "" + [x] -> "${x}" + [x | rest] -> "${x} ${show(rest)}" + } +} + +pascal(n: int) { + int[] row = [] + for r in 0..n { + for (j = r - 1; j > 0; j -= 1) { + row[j] += row[j - 1] + } + row.push(1) + println(show(row)) + } +} + +main() { + pascal(8) +} + +// Output: +// 1 +// 1 1 +// 1 2 1 +// 1 3 3 1 +// 1 4 6 4 1 +// 1 5 10 10 5 1 +// 1 6 15 20 15 6 1 +// 1 7 21 35 35 21 7 1 diff --git a/examples/08-rosetta/25-prime-decomposition.ae b/examples/08-rosetta/25-prime-decomposition.ae new file mode 100644 index 000000000..0c2bab47c --- /dev/null +++ b/examples/08-rosetta/25-prime-decomposition.ae @@ -0,0 +1,55 @@ +// Prime decomposition: the primes whose product is a given number. +// +// The task returns the prime factors of an n above 1, with no limit set by +// the machine's word size; the last of four numbers here is 2^100 - 1. +// std.bignum does the arithmetic, and with +// `type Big = owned ptr drop bignum.free` each value is freed when its +// local is bound again and when the function returns. Trial division: a +// divisor that divides what is left is prime, smaller primes being gone. +// +// Run: ae run examples/08-rosetta/25-prime-decomposition.ae +// See: https://rosettacode.org/wiki/Prime_decomposition + +import std.bignum +import std.string + +type Big = owned ptr drop bignum.free + +decompose(n: ptr) -> string[] { + string[] factors = [] + rest = bignum.clone(n) as Big + d = bignum.from_int(2) as Big + one = bignum.from_int(1) as Big + while true { + square = bignum.multiply(d, d) as Big + if bignum.compare(square, rest) > 0 { + break + } + r = bignum.remainder(rest, d) as Big + if bignum.is_zero(r) == 1 { + factors.push(bignum.to_decimal(d)) + rest = bignum.divide(rest, d) as Big + } else { + d = bignum.add(d, one) as Big + } + } + if bignum.compare(rest, one) > 0 { + factors.push(bignum.to_decimal(rest)) + } + return factors +} + +main() { + for text in ["12", "1234567", "600851475143", "1267650600228229401496703205375"] { + parsed, _ = bignum.from_decimal(text) + n = parsed as Big + factors = decompose(n) + println("${text} = ${string.join(string.seq_from_strings(factors), " * ")}") + } +} + +// Output: +// 12 = 2 * 2 * 3 +// 1234567 = 127 * 9721 +// 600851475143 = 71 * 839 * 1471 * 6857 +// 1267650600228229401496703205375 = 3 * 5 * 5 * 5 * 11 * 31 * 41 * 101 * 251 * 601 * 1801 * 4051 * 8101 * 268501 diff --git a/examples/README.md b/examples/README.md index 9f8446de0..903973534 100644 --- a/examples/README.md +++ b/examples/README.md @@ -138,6 +138,48 @@ Whole programs, each putting several of the ideas above to work. | [03-build-supervisor.ae](07-applications/03-build-supervisor.ae) | `os.run_supervised`: a deadline, a process group, nothing left behind | [stdlib reference](../docs/stdlib-reference.md) | | [04-calculator.ae](07-applications/04-calculator.ae) | a terminal UI: a button grid as a DSL, closures over ref cells | [Closures and lifetimes](../docs/closures-and-lifetimes.md) | +## 8. Rosetta Code + +Classic programming tasks as [Rosetta Code](https://rosettacode.org) +states them, each a whole program whose output CI checks like the rest +(#1191). The lines are counted the same way for each language: a +non-blank line that is not only a comment, for Aether's file here (its +header and Output block left out) and for the first Go and Python +solution in [RosettaCodeData](https://github.com/acmeism/RosettaCodeData) +(1d475861, 2026-07-16). The other languages' first files are not always +whole programs (Python's GCD imports `fractions.gcd`, Go's leap year is the +function alone), so the counts compare length, not like for like. + +| Example | Task | Lines: Aether, Go, Python | +|---|---|---| +| [01-fizzbuzz.ae](08-rosetta/01-fizzbuzz.ae) | [FizzBuzz](https://rosettacode.org/wiki/FizzBuzz) | 13, 16, 9 | +| [02-ackermann.ae](08-rosetta/02-ackermann.ae) | [Ackermann function](https://rosettacode.org/wiki/Ackermann_function) | 12, 9, 3 | +| [03-fibonacci.ae](08-rosetta/03-fibonacci.ae) | [Fibonacci sequence](https://rosettacode.org/wiki/Fibonacci_sequence) | 22, 6, 10 | +| [04-factorial.ae](08-rosetta/04-factorial.ae) | [Factorial](https://rosettacode.org/wiki/Factorial) | 12, 19, 2 | +| [05-gcd.ae](08-rosetta/05-gcd.ae) | [Greatest common divisor](https://rosettacode.org/wiki/Greatest_common_divisor) | 9, 35, 1 | +| [06-sieve.ae](08-rosetta/06-sieve.ae) | [Sieve of Eratosthenes](https://rosettacode.org/wiki/Sieve_of_Eratosthenes) | 22, 33, 7 | +| [07-hanoi.ae](08-rosetta/07-hanoi.ae) | [Towers of Hanoi](https://rosettacode.org/wiki/Towers_of_Hanoi) | 9, 25, 6 | +| [08-n-queens.ae](08-rosetta/08-n-queens.ae) | [N-queens problem](https://rosettacode.org/wiki/N-queens_problem) | 50, 51, 7 | +| [09-reverse-string.ae](08-rosetta/09-reverse-string.ae) | [Reverse a string](https://rosettacode.org/wiki/Reverse_a_string) | 13, 63, 1 | +| [10-palindrome.ae](08-rosetta/10-palindrome.ae) | [Palindrome detection](https://rosettacode.org/wiki/Palindrome_detection) | 26, 11, 2 | +| [11-roman-encode.ae](08-rosetta/11-roman-encode.ae) | [Roman numerals/Encode](https://rosettacode.org/wiki/Roman_numerals/Encode) | 17, 32, 2 | +| [13-binary-digits.ae](08-rosetta/13-binary-digits.ae) | [Binary digits](https://rosettacode.org/wiki/Binary_digits) | 7, 9, 17 | +| [14-quicksort.ae](08-rosetta/14-quicksort.ae) | [Sorting algorithms/Quicksort](https://rosettacode.org/wiki/Sorting_algorithms/Quicksort) | 36, 66, 19 | +| [15-binary-search.ae](08-rosetta/15-binary-search.ae) | [Binary search](https://rosettacode.org/wiki/Binary_search) | 20, 12, 9 | +| [16-hailstone.ae](08-rosetta/16-hailstone.ae) | [Hailstone sequence](https://rosettacode.org/wiki/Hailstone_sequence) | 23, 29, 14 | +| [19-levenshtein.ae](08-rosetta/19-levenshtein.ae) | [Levenshtein distance](https://rosettacode.org/wiki/Levenshtein_distance) | 27, 34, 17 | +| [20-happy-numbers.ae](08-rosetta/20-happy-numbers.ae) | [Happy numbers](https://rosettacode.org/wiki/Happy_numbers) | 16, 26, 10 | +| [21-perfect-numbers.ae](08-rosetta/21-perfect-numbers.ae) | [Perfect numbers](https://rosettacode.org/wiki/Perfect_numbers) | 18, 29, 6 | +| [22-look-and-say.ae](08-rosetta/22-look-and-say.ae) | [Look-and-say sequence](https://rosettacode.org/wiki/Look-and-say_sequence) | 22, 28, 17 | +| [23-josephus.ae](08-rosetta/23-josephus.ae) | [Josephus problem](https://rosettacode.org/wiki/Josephus_problem) | 27, 39, 13 | +| [24-pascals-triangle.ae](08-rosetta/24-pascals-triangle.ae) | [Pascal's triangle](https://rosettacode.org/wiki/Pascal's_triangle) | 20, 35, 9 | +| [25-prime-decomposition.ae](08-rosetta/25-prime-decomposition.ae) | [Prime decomposition](https://rosettacode.org/wiki/Prime_decomposition) | 34, 35, 52 | + +Over these 22 tasks: Aether 455 lines, Go 642, Python 233. Three tasks of +the tranche wait for compiler fixes and are left out rather than written +around them: Roman numerals/Decode and Leap year (#3478, an `if` value +whose condition ends in a call) and Caesar cipher (#3479, `c = s[i]`). + ## Writing an example An example is one `.ae` file, or a directory when it needs more than one diff --git a/new_changelogs/20261030T150000Z-added-1191-rosetta-code-tranche.md b/new_changelogs/20261030T150000Z-added-1191-rosetta-code-tranche.md new file mode 100644 index 000000000..6195bf936 --- /dev/null +++ b/new_changelogs/20261030T150000Z-added-1191-rosetta-code-tranche.md @@ -0,0 +1,6 @@ +- **Rosetta Code in the examples (#1191).** `examples/08-rosetta/` holds a + first tranche of 22 classic tasks (FizzBuzz to prime decomposition with + std.bignum), each a whole program whose output `make check-examples` + checks, and the path's README compares their length with the first Go + and Python solutions in RosettaCodeData. Three more tasks wait for + #3478 and #3479. From 6d0f026480d0b18b1ee6686de17069338373618c Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 16:22:16 -0300 Subject: [PATCH 06/13] docs: the glossary says a scheduler starts at most 16 cores (#1604) 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. --- docs/glossary.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index 0117656cd..6ac97ea22 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -44,8 +44,8 @@ clause. [Contracts](language-reference.md#function-contracts-requires--ensures) **cooperative scheduler.** The scheduler for targets without threads (WebAssembly, ESP32): every actor runs on one thread, stepped in turn. -**core.** One of the scheduler's threads, one per CPU by default. Each runs -the actors assigned to it. +**core.** One of the scheduler's threads: one per CPU the process may use, +up to 16. Each runs the actors assigned to it. **counted value.** A string, closure, or a struct, array, tuple or optional made from them. Every place that holds one owns one reference, and the From d9dec8a045c93aae8f91efdca1cf213c0b4db67b Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 16:49:32 -0300 Subject: [PATCH 07/13] docs, examples: what the review of the whole diff found (#3475, #3476, #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). --- README.md | 4 +- docs/bootstrap-from-source.md | 5 +- docs/cic-help.md | 4 +- docs/getting-started.md | 22 ++-- docs/glossary.md | 36 ++++-- docs/tutorial.md | 113 ++++++++++++++---- examples/01-language/11-arrays-and-slices.ae | 7 +- examples/01-language/12-collections.ae | 1 - examples/01-language/17-ownership.ae | 9 +- examples/02-actors/03-ask-reply.ae | 8 +- examples/02-actors/11-ending.ae | 15 +-- examples/02-actors/14-bounded-mailbox.ae | 8 +- examples/02-actors/15-streams.ae | 10 +- examples/03-std/01-files.ae | 1 - examples/03-std/10-tcp.ae | 105 ++++++++-------- examples/03-std/11-cryptography.ae | 23 +++- examples/04-interop/02-own-c/main.ae | 1 - examples/04-interop/07-limits/host.c | 5 +- examples/04-interop/07-limits/script.ae | 9 +- .../07-applications/01-word-count/main.ae | 7 +- examples/08-rosetta/06-sieve.ae | 3 +- examples/08-rosetta/08-n-queens.ae | 2 - examples/08-rosetta/19-levenshtein.ae | 3 +- examples/08-rosetta/23-josephus.ae | 1 - examples/README.md | 4 +- ...030T130000Z-added-3476-batch-9-examples.md | 2 +- ...T130100Z-changed-3475-one-learning-path.md | 5 + .../crypto_tls13_chain/test_tls13_chain.ae | 7 +- 28 files changed, 261 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 898ec0bbb..dd46f1392 100644 --- a/README.md +++ b/README.md @@ -392,8 +392,8 @@ Same file is config, validation, conditional logic, and the entry point. No seco **Learn Aether**, in this order: - [Getting Started](docs/getting-started.md) - Install, a first program, a project, the tools -- [The path through the examples](examples/README.md) - One idea per file, from the first program to actors, std, C, the sandbox and whole programs; every output checked in CI -- [The tutorial](docs/tutorial.md) - The path told in prose, a chapter per part, with common mistakes and exercises +- [The path through the examples](examples/README.md) - One idea per file, from the first program to actors, std, C, the sandbox and whole programs; CI builds every one and checks the output of each that runs unattended +- [The tutorial](docs/tutorial.md) - The path told in prose, a chapter per part, with common mistakes, exercises and a syntax quick reference - [Glossary](docs/glossary.md) - The words the others use, defined - [Language Reference](docs/language-reference.md) - Complete language specification diff --git a/docs/bootstrap-from-source.md b/docs/bootstrap-from-source.md index 1ca53ef63..3816b712d 100644 --- a/docs/bootstrap-from-source.md +++ b/docs/bootstrap-from-source.md @@ -286,8 +286,9 @@ Rules of thumb: - [install-layout.md](install-layout.md), exactly what `make install` / `install.sh` lay down, and the MANIFEST contract aeb relies on. -- [getting-started.md](getting-started.md), first program, project - config, error handling, the language tour. +- [getting-started.md](getting-started.md), installing, a first program, + a project and the tools; the language itself is taught by the path in + [examples/README.md](../examples/README.md) and [tutorial.md](tutorial.md). - [emit-lib.md](emit-lib.md), building Aether as a `.so`/`.dylib` for FFI hosts (`--emit=lib`). - [CONTRIBUTING.md](../CONTRIBUTING.md), the contributor flow, including diff --git a/docs/cic-help.md b/docs/cic-help.md index 5d0f47857..c96bee616 100644 --- a/docs/cic-help.md +++ b/docs/cic-help.md @@ -368,5 +368,5 @@ stands alone. `ae help` introspects. - [`closures-and-builder-dsl.md`](closures-and-builder-dsl.md), the syntax/semantics the help command's pattern matcher walks. -- [`getting-started.md`](getting-started.md), where the help command - itself should be cross-linked once it ships. +- [`getting-started.md`](getting-started.md), which introduces the help + command to newcomers ("Tools for When You Are Stuck"). diff --git a/docs/getting-started.md b/docs/getting-started.md index 5e3b75ca0..8308313dd 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -164,7 +164,7 @@ ae test `ae init` writes `aether.toml`, `src/main.ae` and `tests/`. `ae run` and `ae build` in the directory build the program `aether.toml` names, and `ae test` runs the tests under `tests/` ([Testing](testing.md)). The -configuration: +configuration `ae init` writes: ```toml [package] @@ -172,6 +172,8 @@ name = "myproject" version = "0.1.0" description = "A new Aether project" license = "MIT" +# modules = "mylib, mylib/internal" # Importable modules, if this is a library +# modules = "." # Export the package ROOT (for a core/*.ae package whose modules import each other as import core.*) [[bin]] name = "myproject" @@ -181,10 +183,11 @@ path = "src/main.ae" [build] target = "native" -# link_flags = "-lsqlite3 -lcurl" # Link external C libraries +# link_flags = "-lsqlite3 -lcurl" # Add extra linker flags ``` -`link_flags` links C libraries a program calls through `extern` +`modules` is for a package other projects import. `link_flags` links C +libraries a program calls through `extern` ([C interoperability](c-interop.md)). A project's own modules live in `lib//module.ae`; [the modules example](../examples/01-language/14-modules/main.ae) shows one, and [`--lib`](../examples/06-tooling/04-lib-path/main.ae) adds @@ -231,12 +234,13 @@ Three documents, each with one job: 1. **[The path](../examples/README.md)**: the examples in order, from the first program to actors, the standard library, C, the sandbox, the - tools and whole programs. Each is one file that teaches one idea, says - how to run it, and ends with the output it prints; CI checks that it - still does. + tools and whole programs. Each is one file that teaches one idea and + says how to run it. CI builds every one, and runs those that run + unattended to check they still print the output written at their end. 2. **[The tutorial](tutorial.md)**: the path told as a story. A chapter per - part, saying what each step shows and why it comes there, the mistakes - newcomers make, and exercises with their answers. + part, saying what each step shows and why it comes there; the first two + chapters add the mistakes newcomers make and exercises with answers, + and a quick reference of the syntax closes it. 3. **[The language reference](language-reference.md)**: everything, to look up. The [glossary](glossary.md) defines the words the others use. @@ -359,7 +363,7 @@ builds a few programs (hello-world, a stateful actor, an actor pipeline, ping-po - Verify: `gcc --version` should show MinGW-w64 GCC **"pthread.h: No such file or directory"** -- Linux: `sudo apt-get install libpthread-stubs0-dev` +- Linux: the C library's development headers are missing: `build-essential` brings them on Debian and Ubuntu (`libc6-dev`), and the other distributions' packages listed under Prerequisites do too - Windows: The Aether runtime uses Win32 threads natively, no pthread library needed **A function from the standard library is undefined** diff --git a/docs/glossary.md b/docs/glossary.md index 6ac97ea22..a89720f1c 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -14,8 +14,9 @@ goes into variables, messages and state. `actor_ref[Member]` is a reference to an actor of one kind. **ask (`?`).** `ref ? Msg { ... }` sends a message and waits for the -handler's `reply`. It ends at once with an error when the actor is dead or -its mailbox is full, and after 5 seconds when no reply comes. +handler's `reply`. It ends at once with an error when the actor is dead, +its mailbox is full, or its handler returns without replying, and after 5 +seconds when the handler has not returned. [Ask pattern](language-reference.md#ask-pattern-request-reply) **audit trail.** The record of every check the sandbox made, allowed or @@ -47,9 +48,10 @@ clause. [Contracts](language-reference.md#function-contracts-requires--ensures) **core.** One of the scheduler's threads: one per CPU the process may use, up to 16. Each runs the actors assigned to it. -**counted value.** A string, closure, or a struct, array, tuple or optional -made from them. Every place that holds one owns one reference, and the -compiler releases it; the program writes no free. +**counted value.** A string, a closure, a string sequence, or a struct, +tuple, optional, sum value or fixed-size array made from them. Every place +that holds one owns one reference, and the compiler releases it; the +program writes no free. [Memory management](memory-management.md) **credit.** In `std.stream`, the right to send one more chunk. The consumer @@ -86,8 +88,9 @@ environment variable, a program, a host. A sandbox with no grants allows nothing. [The containment sandbox](containment-sandbox.md) **handle.** A `ptr` a module gave the program: a list, a map, a JSON -document, a socket, a buffer from `make`. The program frees it, through its -module, usually with `defer` next to the call that made it. +document, a socket, a `heap.new` box, memory from C. A `ptr` owns nothing, +so the program frees it, through its module, usually with `defer` next to +the call that made it. **handler.** One arm of an actor's `receive` block: what it does with one kind of message. @@ -95,7 +98,8 @@ kind of message. **interpolation.** `"${expr}"`: a value written into a string. **link.** A tie between two actors: when one ends, the other ends too, -unless it traps exits. [Links](actor-concurrency.md#links) +with the same reason, unless it traps exits. An end whose reason is +"normal" (a stop) ends nobody. [Links](actor-concurrency.md#links) **mailbox.** Where an actor's messages wait until it handles them. @@ -116,7 +120,8 @@ project. [Modules](language-reference.md#modules-and-imports) [Optionals](language-reference.md#optionals) **outcome.** How a call into an exported function ended, from -`aether_last_outcome()`: ok, panicked, deadline, cancelled, memory cap. +`aether_last_outcome()`: ok, panicked, deadline, cancelled, memory cap, +out of memory, or incompatible (a library made for another runtime). **panic.** What a bug raises: it unwinds to the nearest `try`, and in an actor's handler it ends that actor. Outside both, it ends the program. @@ -133,7 +138,9 @@ gives a fallback, and `!` passes the error up. the panic barrier, the sandbox. **safe point.** A place where a library's code checks whether its call -should end (a deadline, a cancel): every loop head, under `--emit=lib`. +should end (a deadline, a cancel): every loop head of code built as a +library (`--emit=lib`, `--emit=obj`), and every few thousand values a JSON +parse reads. **sandbox.** A list of grants that `sandbox.enforce` holds a block of code to. [The containment sandbox](containment-sandbox.md) @@ -146,7 +153,9 @@ delivers their messages. **send (`!`).** `ref ! Msg { ... }`: a message on its way, without waiting. **slice (`T[]`).** A view of elements stored elsewhere: a pointer and a -length. `xs[lo..hi]` makes one. [Slices](language-reference.md#slices-t) +length. `xs[lo..hi]` makes one. The buffer `make([]T, n)` hands out has one +owner, the local that holds it, which releases it when its scope ends: the +program writes no free. [Slices](language-reference.md#slices-t) **spawn.** `spawn(Name())` starts an actor and returns its `actor_ref`. @@ -154,8 +163,9 @@ length. `xs[lo..hi]` makes one. [Slices](language-reference.md#slices-t) `state name = value`. [State variables](language-reference.md#state-variables) **std, contrib.** `std/` is the standard library every install has; -`contrib/` holds modules that need a library or a toolchain of their own -(a database, another language). +`contrib/` holds modules outside it, such as bridges to a database or to +another language, which a build may leave out. +[std or contrib](stdlib-vs-contrib.md) **step.** One run of an actor's handler for one message. diff --git a/docs/tutorial.md b/docs/tutorial.md index 52141794e..c106a2fe1 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -3,8 +3,10 @@ This tutorial is the story told alongside [the path through the examples](../examples/README.md). The path is the code: one file per idea, each saying how to run it and ending with what it prints. This page says -what each step shows, why it comes where it does, and what newcomers get -wrong, and each chapter ends with exercises and their answers. +what each step shows and why it comes where it does. The first two +chapters, the language and actors, also list the mistakes newcomers make +and end with exercises and their answers. A [quick reference](#quick-reference) +of the syntax closes the page. Read a chapter, then run its examples in order from the repository root and change them: @@ -46,7 +48,7 @@ and the one stateful thing with an inside and an outside is the actor A line ends a statement, and `${...}` puts any value into a string. A local takes its type from the first value it is given and keeps it. Conditions -take no parentheses, every body takes braces, and `if` is also a value: +need no parentheses, every body takes braces, and `if` is also a value: ```aether,run main() { @@ -247,11 +249,17 @@ statement however the block ends. Ownership is the last step of the chapter because it explains the others. Aether has no garbage collector, and it does not ask you to free -everything either. Strings, closures, and the structs, arrays and -optionals made from them are counted: the compiler frees them, and you -write no free. A *handle*, a `ptr` a module gave you (a list, a map, a -JSON document, a buffer), is the program's to free, through its module, -and `defer` keeps the free next to the `new`: +everything either. Three rules cover it: + +- **Counted values**: strings, closures, string sequences, and the + structs, tuples, optionals and fixed-size arrays made from them. The + compiler counts them and frees them; you write no free. +- **A slice's buffer** (`make([]T, n)`, or one `push` grows) has one owner, + the local that holds it, and is released when that local's scope ends. + You write no free for it either. +- **A handle**, a `ptr` a module gave you (a list, a map, a JSON document, + a socket), is the program's to free, through its module, and `defer` + keeps the free next to the `new`: ```aether,run import std.list @@ -271,9 +279,9 @@ main() { ### Common mistakes -- **Freeing a value the compiler counts.** A string needs no free, and - freeing a parameter the function only borrows is refused (E0901). Free - handles, and only handles. +- **Freeing what the compiler frees.** A string or a `make` buffer needs + no free, and freeing a parameter the function only borrows is refused + (E0901). Free handles. - **Forgetting a handle.** A `list.new()` with no `list.free` leaks it. Write the `defer` on the next line. - **Changing a local's type.** `count = 0` then `count = "many"` is a @@ -430,8 +438,9 @@ total: 12 The ask sees both adds: messages from one sender reach an actor in the order they were sent. `wait_for_idle()` is the other way to wait: it -returns once every actor has nothing left to do, which is how a program -lets its actors finish before it ends. +returns once every actor has nothing left to do, receive timeouts that are +counting down included, so main can go on knowing what they did. (The end +of `main` waits for the messages already sent anyway.) ### Actors that talk to each other @@ -487,16 +496,19 @@ bottleneck, its work can be split over several by key. ### Common mistakes -- **Reading an actor's state from outside.** Ask it (`?`), or have it send - what it knows; its state is its own. +- **Reading an actor's state while it runs.** A reference reaches an + actor's state (`c.total`), but read while the actor is handling messages + it races them. Ask it (`?`), or read after `wait_for_idle()`. - **Expecting order between two senders.** Main and another actor sending to the same actor interleave in any order. Wait for one before the other, or route both through one sender. - **Calling `wait_for_idle()` inside an actor.** It waits for every actor, this one too, so it can never return there; it panics instead. Call it from main. -- **Blocking in a handler.** A handler that sleeps or waits on a socket - holds a core. Keep handlers short, and give slow work its own actor. +- **Blocking in a handler.** A handler that sleeps, waits on a socket or + computes for long stalls every actor that shares its core. Keep handlers + short, and hand blocking work to [std.worker](../std/worker/README.md), + a thread pool outside the scheduler. - **Using a handle after sending it.** A `ptr` sent in a message belongs to the receiver now; the sender must not touch it again. @@ -648,10 +660,13 @@ file a library a C program links, each function an `aether_` export. A program that runs Aether code it does not control bounds it: every -export runs in an execution frame, so a deadline, a cancel from another -thread or a memory budget ends a call and gives the host an outcome, -never a crash. And Aether can host other languages (Python, Lua, Tcl, Go) -inside the program, under the sandbox of chapter 5. [C +export runs in an execution frame, so a panic, a deadline, a cancel from +another thread or a memory budget ends the call and gives the host an +outcome. A deadline or a cancel lands at the call's next safe point, a +loop head, so code with no loop (straight-line code, or a long C call) runs +on until it returns. Aether also hosts other languages under the sandbox +of chapter 5: Python, Lua and Tcl inside the program, a Go program as a +child process, and a TinyGo library in-process. [C interoperability](c-interop.md) and [emit=lib](emit-lib.md) are the references. @@ -712,6 +727,62 @@ classic problems (FizzBuzz, the N queens, quicksort, Levenshtein distance) solved in Aether, with their length beside the Go and Python solutions. They are good exercises too: try one before reading its file. +## Quick reference + +```aether,fragment +// Values +x = 42 // a local's type comes from its value +long big = 3_000_000_000 // or is written before the name +const LIMIT = 10 +name = "Ada" +println("${name} is ${x}") // interpolation + +// Control flow +if x > 10 { ... } else if x > 5 { ... } else { ... } +kind = if x > 10 { "big" } else { "small" } +while x > 0 { x = x - 1 } +for i in 0..10 { ... } // 0 to 9 +for item in xs { ... } + +// Functions +add(a: int, b: int) -> int { return a + b } +square(x: int) -> x * x +split(n: int) -> (int, int) { return n / 2, n % 2 } +greet(name: string, greeting: string = "Hello") -> string { ... } +fact(0) -> 1 +fact(n) when n > 0 -> n * fact(n - 1) + +// Data +struct Point { x: int y: int } +p = Point { x: 1, y: 2 } +enum Color { Red, Green } +type Shape = Circle | Rect +area = match s { Circle -> ... Rect -> ... } +int? maybe = none +v = maybe ?? 0 + +// Failure +parse(s: string) -> int! { ... return 0, "why" } +n = parse("7") or 0 +try { ... } catch msg { ... } +defer list.free(items) + +// Actors +message Add { n: int } +message Total {} +actor Counter { + state total = 0 + receive { + Add(n) -> { total = total + n } + Total() -> { reply total } + } +} +c = spawn(Counter()) +c ! Add { n: 5 } // send +int t = c ? Total {} // ask +wait_for_idle() +``` + ## Where next - [Language reference](language-reference.md): every construct, in full. diff --git a/examples/01-language/11-arrays-and-slices.ae b/examples/01-language/11-arrays-and-slices.ae index 65c817908..bf280a459 100644 --- a/examples/01-language/11-arrays-and-slices.ae +++ b/examples/01-language/11-arrays-and-slices.ae @@ -7,8 +7,8 @@ // `xs[lo..hi]` is a slice of the elements from lo up to hi, sharing them. // `.len` is the length. Every index is checked: one out of range is a // panic naming it. `make([]T, n)` allocates n zeroed elements whose -// length is known only when the program runs; that buffer is a handle, -// and `free` gives it back. +// length is known only when the program runs; the local that holds the +// buffer owns it, and releases it when its scope ends (17-ownership.ae). // // Run: ae run examples/01-language/11-arrays-and-slices.ae // See: docs/language-reference.md#slices-t @@ -59,7 +59,6 @@ main() { println("${names.len} names, last ${names[names.len - 1]}") squares = make([]int, 6) - defer free(squares) for i in 0..squares.len { squares[i] = i * i } @@ -79,4 +78,4 @@ main() { // 2 2 [80, 50] // 3 names, last linus // [0, 1, 4, 9, 16, 25] -// caught: examples/01-language/11-arrays-and-slices.ae:70: array index 7 out of range for length 5 +// caught: examples/01-language/11-arrays-and-slices.ae:69: array index 7 out of range for length 5 diff --git a/examples/01-language/12-collections.ae b/examples/01-language/12-collections.ae index c9c893b0b..1d3e03669 100644 --- a/examples/01-language/12-collections.ae +++ b/examples/01-language/12-collections.ae @@ -42,7 +42,6 @@ main() { keys, _ = map.keys(capitals) countries = make([]string, map.keys_size(keys)) - defer free(countries) for i in 0..countries.len { countries[i] = map.keys_get(keys, i) } diff --git a/examples/01-language/17-ownership.ae b/examples/01-language/17-ownership.ae index 734d4fcc8..4c30d662a 100644 --- a/examples/01-language/17-ownership.ae +++ b/examples/01-language/17-ownership.ae @@ -1,7 +1,7 @@ // Ownership: what the compiler frees, and what the program frees. // // Aether has no garbage collector, and it does not ask you to free -// everything either. Two kinds of value, two rules: +// everything either. Three kinds of value, three rules: // // Values the program owns are counted by the compiler: strings, closures, // string sequences, and the structs, tuples, optionals and arrays built @@ -12,8 +12,13 @@ // caller one reference. The program writes no free for these, and // freeing a parameter it only borrows is refused (E0901). // +// A slice's buffer, the one `make([]T, n)` hands out or `push` grows, has +// one owner instead of a count: the local that holds it, which releases it +// when its scope ends, or whatever it was moved to. The program writes no +// free for it either. +// // Handles are `ptr`s, and a `ptr` owns nothing: a list or a map, a -// `heap.new` box, a `make` buffer, a C allocation. Whoever makes one +// `heap.new` box, a C allocation. Whoever makes one // frees it, with `defer` right after; a function that returns one hands // that duty to its caller. A function that keeps a `ptr` parameter past // the call says so with `@keep` (E0902 otherwise). A string goes into a diff --git a/examples/02-actors/03-ask-reply.ae b/examples/02-actors/03-ask-reply.ae index 75f669c6f..8641abe65 100644 --- a/examples/02-actors/03-ask-reply.ae +++ b/examples/02-actors/03-ask-reply.ae @@ -5,9 +5,11 @@ // first field is the answer (`reply Found { value: v }`); the asker gets a // value of that type, inferred from the handler. Sends from main and asks // from main reach the actor in the order they were written, so an ask sees -// every message sent before it. A handler that never replies leaves the -// asker waiting until the ask times out (5 seconds), and it then gets the -// type's zero. The map in the store's state is a handle: the actor frees +// every message sent before it. A handler that returns without replying +// ends the ask at once, and one that has not returned after 5 seconds +// times it out: either way the asker gets the type's zero, and `v, err =` +// says why ("the handler returned without replying", "ask timed out after +// 5000 ms"). The map in the store's state is a handle: the actor frees // it in the handler that closes it. // // Run: ae run examples/02-actors/03-ask-reply.ae diff --git a/examples/02-actors/11-ending.ae b/examples/02-actors/11-ending.ae index 726c2eb45..fb0cf7040 100644 --- a/examples/02-actors/11-ending.ae +++ b/examples/02-actors/11-ending.ae @@ -1,12 +1,13 @@ // Ending an actor, and what its watchers are told. // -// An actor ends once, by the first of three things: it stops itself -// (`actors.stop()` in its own handler, reason "normal"), something kills it -// (`actors.kill(ref)`, from anywhere, reason "killed"), or its handler -// panics (the panic's message is the reason). An actor that monitors it -// (`actors.monitor(ref)`, from a handler) gets one `Down { ref, reason }` -// when it ends. A message sent to an actor that has ended is dropped and -// counted, never delivered. +// An actor ends once. Here it ends in each of the three common ways: it +// stops itself (`actors.stop()` in its own handler, reason "normal"), +// something kills it (`actors.kill(ref)`, from anywhere, reason "killed"), +// or its handler panics (the panic's message is the reason). The end of an +// actor it is linked to can end it too (the next example). An actor that +// monitors it (`actors.monitor(ref)`, from a handler) gets one +// `Down { ref, reason }` when it ends. A message sent to an actor that has +// ended is dropped and counted, never delivered. // // Run: ae run examples/02-actors/11-ending.ae // See: docs/actor-concurrency.md#the-end-of-an-actor diff --git a/examples/02-actors/14-bounded-mailbox.ae b/examples/02-actors/14-bounded-mailbox.ae index cdbe4fa2e..22edcc01f 100644 --- a/examples/02-actors/14-bounded-mailbox.ae +++ b/examples/02-actors/14-bounded-mailbox.ae @@ -28,22 +28,18 @@ actor Ingest { state handled = 0 receive { Burst(n) -> { - i = 1 - while i <= n { + for i in 1..n + 1 { err = try_send(self, Line { n: i }) if err == "" { println("line ${i}: sent") } else { println("line ${i}: ${err}") } - i = i + 1 } } Flood(n) -> { - i = 1 - while i <= n { + for i in 1..n + 1 { self ! Line { n: i } // past the bound: dropped and counted - i = i + 1 } } Line(n) -> { handled = handled + 1 } diff --git a/examples/02-actors/15-streams.ae b/examples/02-actors/15-streams.ae index 847cc7b56..9d23a990c 100644 --- a/examples/02-actors/15-streams.ae +++ b/examples/02-actors/15-streams.ae @@ -32,13 +32,11 @@ actor Producer { } // Offer lines until the window is full or every line has gone. More() -> { - going = true - while going && sent < total { - if stream.offer(s, "log line ${sent + 1}") == 1 { - sent = sent + 1 - } else { - going = false // no credit left: wait for the next one + while sent < total { + if stream.offer(s, "log line ${sent + 1}") == 0 { + break // no credit left: wait for the next one } + sent = sent + 1 } peak = stream.peak(s) if sent == total && finished == 0 { diff --git a/examples/03-std/01-files.ae b/examples/03-std/01-files.ae index 25fa3163f..c8302726e 100644 --- a/examples/03-std/01-files.ae +++ b/examples/03-std/01-files.ae @@ -55,7 +55,6 @@ main() { return fs.WALK_CONTINUE } names = make([]string, list.size(found)) - defer free(names) for i in 0..names.len { name, _ = list.get_string(found, i) names[i] = name diff --git a/examples/03-std/10-tcp.ae b/examples/03-std/10-tcp.ae index 92f3d3473..5a51f7e0c 100644 --- a/examples/03-std/10-tcp.ae +++ b/examples/03-std/10-tcp.ae @@ -1,11 +1,14 @@ // TCP: a server and a client over a socket, in one program. // -// std.tcp is the layer under std.http: connect, listen, accept, read and -// write, each returning an error string to check. TCP carries a stream of -// bytes, not messages: a read returns what has arrived, which may be part -// of a message or more than one. So a protocol says where a message ends -// (here a newline) and the reader reads until it gets there. The server -// listens on the loopback address only, on a port the system picks. +// TCP is the layer under HTTP: std.tcp connects, listens, accepts, reads +// and writes, and each call returns an error string to check. TCP carries +// a stream of bytes, not messages: a read returns what has arrived, which +// may be part of a message or more than one. So a protocol says where a +// message ends (here a newline), and the reader keeps reading until it gets +// there, keeping whatever came after it for the next message. The server +// listens on the loopback address only, on a port the system picks. One +// thread can play both sides: a connection waits in the listener's queue, +// complete, until the server accepts it. // // Run: ae run examples/03-std/10-tcp.ae // See: std/tcp/README.md @@ -13,63 +16,65 @@ import std.string import std.tcp -message Ask { port: int, text: string } +// A socket, and what has arrived on it past the last line read. +struct LineReader { + sock: ptr + pending: string +} -// Read one line, without its newline, however the bytes arrive. -read_line(sock: ptr) -> (string, string) { - line = "" - while !string.ends_with(line, "\n") { - part, err = tcp.read(sock, 64) +// The next line, without its newline, however its bytes arrive. +read_line(r: *LineReader) -> (string, string) { + while true { + at = string.index_of(r.pending, "\n") + if at >= 0 { + line = string.substring(r.pending, 0, at) + r.pending = string.substring(r.pending, at + 1, string.length(r.pending)) + return line, "" + } + part, err = tcp.read(r.sock, 64) if err != "" { return "", err } - line = string.concat(line, part) + r.pending = string.concat(r.pending, part) } - return string.substring(line, 0, string.length(line) - 1), "" } -// The client: connect, send a line, read the answer. -actor Client { - receive { - Ask(port, text) -> { - sock, err = tcp.connect("127.0.0.1", port) - if err != "" { - println("client: connect failed: ${err}") - } else { - _n, werr = tcp.write(sock, "${text}\n") - if werr == "" { - answer, rerr = read_line(sock) - if rerr == "" { - println("client: the server said '${answer}'") - } - } - tcp.close(sock) - } - } +// Ends the program with a message when a step failed. +check(what: string, err: string) { + if err != "" { + println("${what} failed: ${err}") + exit(1) } } main() { srv, err = tcp.listen_on("127.0.0.1", 0) - if err != "" { - println("listen failed: ${err}") - return - } - client = spawn(Client()) - client ! Ask { port: tcp.server_port(srv), text: "hello over tcp" } + check("listen", err) + defer tcp.server_close(srv) - // Serve one connection: read a line, answer it in capitals. - sock, aerr = tcp.accept(srv) - if aerr == "" { - line, rerr = read_line(sock) - if rerr == "" { - println("server: got '${line}'") - _n, _werr = tcp.write(sock, "${string.to_upper(line)}\n") - } - tcp.close(sock) - } - wait_for_idle() - tcp.server_close(srv) + // The client connects and sends its line. + client, cerr = tcp.connect("127.0.0.1", tcp.server_port(srv)) + check("connect", cerr) + defer tcp.close(client) + _sent, werr = tcp.write(client, "hello over tcp\n") + check("the client's write", werr) + + // The server takes the connection, reads the line, answers in capitals. + conn, aerr = tcp.accept(srv) + check("accept", aerr) + defer tcp.close(conn) + server_side = LineReader { sock: conn, pending: "" } + line, rerr = read_line(&server_side) + check("the server's read", rerr) + println("server: got '${line}'") + _answered, aw = tcp.write(conn, "${string.to_upper(line)}\n") + check("the server's write", aw) + + // The client reads the answer. + client_side = LineReader { sock: client, pending: "" } + answer, ar = read_line(&client_side) + check("the client's read", ar) + println("client: the server said '${answer}'") } // Output: diff --git a/examples/03-std/11-cryptography.ae b/examples/03-std/11-cryptography.ae index 8509fd0f7..12a729f52 100644 --- a/examples/03-std/11-cryptography.ae +++ b/examples/03-std/11-cryptography.ae @@ -8,7 +8,10 @@ // and anyone with the public key can check it. // They take their input as a `byte[]` slice: `string.bytes(s)` is a // string's. What they return as a `ptr` is a std.bytes buffer the program -// owns and frees; `wipe_free` zeroes a secret before it lets it go. +// owns and frees; `wipe_free` zeroes a secret before it lets it go. A tag +// received is compared with `ct.bytes_eq_slice`, which takes as long +// whatever the bytes hold: `==` stops at the first difference, and how +// long it took tells an attacker how much of a forged tag was right. // // Run: ae run examples/03-std/11-cryptography.ae // See: docs/stdlib-reference.md#cryptography-stdcryptography @@ -16,20 +19,28 @@ import std.bytes import std.encoding import std.string +import std.cryptography.ct import std.cryptography.ed25519 import std.cryptography.hmac import std.cryptography.sha2 +// Whether a received tag is the one the key makes for the message. +tag_ok(key: string, msg: string, received: ptr) -> bool { + expected = hmac.hmac_sha256(string.bytes(key), string.bytes(msg)) + same = ct.bytes_eq_slice(bytes.view(expected), bytes.view(received)) == 1 + bytes.free(expected) + return same +} + main() { // A hash: the same input always gives the same 32 bytes. println("sha256(abc) = ${sha2.sha256_hex(string.bytes("abc"))}") // An HMAC: only a holder of the key can make the same tag. msg = "pay 10 to alice" - tag = hmac.hmac_sha256_hex(string.bytes("shared key"), string.bytes(msg)) - again = hmac.hmac_sha256_hex(string.bytes("shared key"), string.bytes(msg)) - other = hmac.hmac_sha256_hex(string.bytes("wrong key"), string.bytes(msg)) - println("hmac matches with the key: ${tag == again}, with another key: ${tag == other}") + tag = hmac.hmac_sha256(string.bytes("shared key"), string.bytes(msg)) + println("hmac checks with the key: ${tag_ok("shared key", msg, tag)}, with another key: ${tag_ok("wrong key", msg, tag)}") + bytes.free(tag) // A signature. The 32-byte private seed comes from a hash here so the // output is the same every run; a real one comes from random bytes @@ -49,7 +60,7 @@ main() { // Output: // sha256(abc) = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad -// hmac matches with the key: true, with another key: false +// hmac checks with the key: true, with another key: false // public key: d1e633b86e5acea0fb20c459575bb672fed1e2c97af24561911bbeedd4ec96c3 // signature verifies: true // on a changed message: false diff --git a/examples/04-interop/02-own-c/main.ae b/examples/04-interop/02-own-c/main.ae index f71aeef45..0063505ec 100644 --- a/examples/04-interop/02-own-c/main.ae +++ b/examples/04-interop/02-own-c/main.ae @@ -29,7 +29,6 @@ main() { println("fnv1a(\"aether\") = ${shim_fnv1a("aether")}") squares = make([]int, 5) - defer free(squares) shim_fill_squares(squares, squares.len) println("squares: ${squares[0]} ${squares[1]} ${squares[2]} ${squares[3]} ${squares[4]}") diff --git a/examples/04-interop/07-limits/host.c b/examples/04-interop/07-limits/host.c index c72846f78..43e1fcb5c 100644 --- a/examples/04-interop/07-limits/host.c +++ b/examples/04-interop/07-limits/host.c @@ -34,7 +34,7 @@ static void sleep_ms(int ms) { #ifdef _WIN32 Sleep((DWORD)ms); #else - struct timespec t = { 0, (long)ms * 1000000L }; + struct timespec t = { ms / 1000, (long)(ms % 1000) * 1000000L }; nanosleep(&t, NULL); #endif } @@ -47,7 +47,8 @@ static void* cancel_soon(void* arg) { } int main(void) { - /* A deadline for every call this thread makes, until it is set to 0. */ + /* A deadline 50 ms from now for this thread's calls (one instant, not a + * budget per call); 0 clears it. */ aether_set_call_deadline(50); report("spin under a 50 ms deadline", aether_spin()); aether_set_call_deadline(0); diff --git a/examples/04-interop/07-limits/script.ae b/examples/04-interop/07-limits/script.ae index 1e77feb3b..25896dd61 100644 --- a/examples/04-interop/07-limits/script.ae +++ b/examples/04-interop/07-limits/script.ae @@ -5,8 +5,9 @@ // scripts, an editor's plugins) bounds what that code may take. Every // exported function runs in an execution frame, so however a call ends, // the host gets control back with an outcome (`aether_last_outcome()`): -// - a deadline (`aether_set_call_deadline(ms)`) ends a call that runs too -// long, at its next loop head, with AETHER_DEADLINE; +// - a deadline (`aether_set_call_deadline(ms)`, an instant ms from when it +// is set, for this thread's calls) ends a call still running past it, +// at its next loop head, with AETHER_DEADLINE; // - another thread ends a running call with `aether_exec_cancel(token)`, // where the token is `aether_exec_token()` of the calling thread, with // AETHER_CANCELLED; @@ -34,10 +35,8 @@ spin() -> int { // The length of a string of n * 16 bytes, built a piece at a time. grow(n: int) -> int { s = "" - i = 0 - while i < n { + for i in 0..n { s = string.concat(s, "0123456789abcdef") - i = i + 1 } return string.length(s) } diff --git a/examples/07-applications/01-word-count/main.ae b/examples/07-applications/01-word-count/main.ae index 8051f2a80..a25144d20 100644 --- a/examples/07-applications/01-word-count/main.ae +++ b/examples/07-applications/01-word-count/main.ae @@ -5,8 +5,8 @@ // words (std.sort) so that equal ones sit together, and counts each run. // Then the five most common, ties in alphabetical order. A file that // cannot be read is a message and exit status 1. The arrays come from -// `make` and go with `defer free`; every string is released by the -// compiler. +// `make`, and each is released with the local that owns it; every string +// is released by the compiler. // // Run: ae run examples/07-applications/01-word-count/main.ae -- examples/07-applications/01-word-count/sample.md // See: docs/language-reference.md @@ -56,7 +56,6 @@ main() { defer list.free(found) words_of(text, found) words = make([]string, list.size(found)) - defer free(words) for i in 0..words.len { w, _ = list.get_string(found, i) words[i] = w @@ -65,9 +64,7 @@ main() { // Equal words are neighbours now: one entry per run. distinct = make([]string, words.len) - defer free(distinct) counts = make([]int, words.len) - defer free(counts) kinds = 0 for i in 0..words.len { if kinds > 0 && distinct[kinds - 1] == words[i] { diff --git a/examples/08-rosetta/06-sieve.ae b/examples/08-rosetta/06-sieve.ae index 8ff6981ca..54385949a 100644 --- a/examples/08-rosetta/06-sieve.ae +++ b/examples/08-rosetta/06-sieve.ae @@ -2,7 +2,7 @@ // // The task is the sieve itself: cross out the multiples of each prime // found, from its square up, and what stays uncrossed is prime. A `make` -// buffer of bools holds the marks, freed by the `defer` beside it, and the +// buffer of bools holds the marks, released with its local, and the // primes go into a slice that grows with `push`. The limit here is 100. // // Run: ae run examples/08-rosetta/06-sieve.ae @@ -10,7 +10,6 @@ primes_up_to(limit: int) -> int[] { crossed = make([]bool, limit + 1) - defer free(crossed) int[] primes = [] for n in 2..limit + 1 { if !crossed[n] { diff --git a/examples/08-rosetta/08-n-queens.ae b/examples/08-rosetta/08-n-queens.ae index c9dcc67e0..37aa71c22 100644 --- a/examples/08-rosetta/08-n-queens.ae +++ b/examples/08-rosetta/08-n-queens.ae @@ -54,9 +54,7 @@ draw(cols: int[]) { main() { for n in 1..11 { cols = make([]int, n) - defer free(cols) first = make([]int, n) - defer free(first) first[0] = -1 println("${n} x ${n}: ${place(cols, 0, first)}") if n == 8 { diff --git a/examples/08-rosetta/19-levenshtein.ae b/examples/08-rosetta/19-levenshtein.ae index ed33d667a..269d281d0 100644 --- a/examples/08-rosetta/19-levenshtein.ae +++ b/examples/08-rosetta/19-levenshtein.ae @@ -3,7 +3,7 @@ // The task shows the distance from "kitten" to "sitting", which is 3; the // task's "rosettacode" to "raisethysword" (8), the same reversed, and // "saturday" to "sunday" are shown too. The table is kept one row at a -// time, in a `make` buffer freed by the `defer` beside it: each cell is the +// time, in a `make` buffer its local owns and releases: each cell is the // cheapest of a deletion, an insertion and a substitution, free when the // two characters agree. // @@ -16,7 +16,6 @@ import std.string levenshtein(a: string, b: string) -> int { n = string.length(b) row = make([]int, n + 1) - defer free(row) for j in 0..n + 1 { row[j] = j } diff --git a/examples/08-rosetta/23-josephus.ae b/examples/08-rosetta/23-josephus.ae index db9daaeba..e8419cbd0 100644 --- a/examples/08-rosetta/23-josephus.ae +++ b/examples/08-rosetta/23-josephus.ae @@ -13,7 +13,6 @@ survivor(n: int, k: int) -> int -> (survivor(n - 1, k) + k) % n killing_order(n: int, k: int) -> int[] { dead = make([]bool, n) - defer free(dead) int[] order = [] at = n - 1 while order.len < n { diff --git a/examples/README.md b/examples/README.md index 903973534..6c05babdc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -147,8 +147,8 @@ non-blank line that is not only a comment, for Aether's file here (its header and Output block left out) and for the first Go and Python solution in [RosettaCodeData](https://github.com/acmeism/RosettaCodeData) (1d475861, 2026-07-16). The other languages' first files are not always -whole programs (Python's GCD imports `fractions.gcd`, Go's leap year is the -function alone), so the counts compare length, not like for like. +whole programs (Python's GCD imports `fractions.gcd`, and its string +reversal is `input()[::-1]`), so the counts compare length, not like for like. | Example | Task | Lines: Aether, Go, Python | |---|---|---| diff --git a/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md b/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md index 2e3ed9867..a0ad0035d 100644 --- a/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md +++ b/new_changelogs/20261030T130000Z-added-3476-batch-9-examples.md @@ -1,4 +1,4 @@ -- **The examples path teaches what batch 9 added (#3476).** Six new +- **The examples path teaches what batch 9 added (#3476).** Seven new examples, each with its output checked by `make check-examples`: ending an actor (`actors.stop`, `actors.kill`, a panic, and the reason a monitor's `Down` carries), links and trapping exits, a bounded mailbox diff --git a/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md b/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md index 2bbf3ac40..2270e2e1d 100644 --- a/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md +++ b/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md @@ -8,3 +8,8 @@ by `make check-docs`), and `docs/tutorials/` is folded into it. The README's documentation list starts with the path. The package-publishing details moved from getting-started to `docs/package-fetching.md`. + The path also says what the runtime does where it did not: a `make` + buffer has one owner, the local that releases it (11-arrays-and-slices + and 17-ownership called it a handle the program frees, and five examples + freed one by hand), and an ask whose handler returns without replying + ends at once (03-ask-reply said it waited out the 5-second timeout). diff --git a/tests/integration/crypto_tls13_chain/test_tls13_chain.ae b/tests/integration/crypto_tls13_chain/test_tls13_chain.ae index 8b39b0208..b76cf7d2f 100644 --- a/tests/integration/crypto_tls13_chain/test_tls13_chain.ae +++ b/tests/integration/crypto_tls13_chain/test_tls13_chain.ae @@ -392,7 +392,12 @@ main() { // A trust anchor's own pathLenConstraint holds, as on OpenSSL and Go // (#3432): a root with pathLenConstraint 0 allows no CA below it. The // root without the extension is "the same leaf under its root" above. - fails = fails + check("a CA below a pathLenConstraint 0 root", anchor_result(pem_rootpl0(), pem_plleaf(), pem_icapl()), 0) + pl_why = anchor_result(pem_rootpl0(), pem_plleaf(), pem_icapl()) + fails = fails + check("a CA below a pathLenConstraint 0 root", pl_why, 0) + if string.equals(pl_why, "a CA's path length constraint is exceeded") != 1 { + println("FAIL a CA below a pathLenConstraint 0 root: refused, but not for its path length (${pl_why})") + fails = fails + 1 + } fails = fails + check("an end entity of a pathLenConstraint 0 root", anchor_result(pem_rootpl0(), pem_pldirect(), ""), 1) // A pinned self-signed leaf with basicConstraints cA FALSE: trusted as From 06e8b781550952f4374c8fcaf39c08d18930bb67 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 17:05:00 -0300 Subject: [PATCH 08/13] docs, examples: what the review of the fixes found (#3475, #3476) The second and last review, of d9dec8a0 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. --- docs/glossary.md | 5 ++-- docs/tutorial.md | 38 +++++++++++++++------------- examples/01-language/17-ownership.ae | 8 +++--- examples/02-actors/15-streams.ae | 4 +-- examples/03-std/10-tcp.ae | 7 +++-- examples/README.md | 14 +++++----- 6 files changed, 43 insertions(+), 33 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index a89720f1c..f0e99cc86 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -154,8 +154,9 @@ delivers their messages. **slice (`T[]`).** A view of elements stored elsewhere: a pointer and a length. `xs[lo..hi]` makes one. The buffer `make([]T, n)` hands out has one -owner, the local that holds it, which releases it when its scope ends: the -program writes no free. [Slices](language-reference.md#slices-t) +owner: a local that holds it releases it when its scope ends. Put in a +field, a module `var`, a tuple or a message, or held by a local a closure +captures, it is the program's to `free`. [Slices](language-reference.md#slices-t) **spawn.** `spawn(Name())` starts an actor and returns its `actor_ref`. diff --git a/docs/tutorial.md b/docs/tutorial.md index c106a2fe1..129964720 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -252,11 +252,14 @@ Aether has no garbage collector, and it does not ask you to free everything either. Three rules cover it: - **Counted values**: strings, closures, string sequences, and the - structs, tuples, optionals and fixed-size arrays made from them. The - compiler counts them and frees them; you write no free. -- **A slice's buffer** (`make([]T, n)`, or one `push` grows) has one owner, - the local that holds it, and is released when that local's scope ends. - You write no free for it either. + structs, tuples, optionals, sum values and fixed-size arrays made from + them. The compiler counts them and frees them; you write no free. +- **A slice's buffer** (`make([]T, n)`, or one `push` grows) has one owner. + A local that holds it releases it when its scope ends, as does a local, + a caller or a `@keep` parameter it moves to. Put in a struct's field, a + module `var`, a tuple or a message, or held by a local a closure + captures, it is yours to give back with `free` (`free(s.items)` empties + the field). - **A handle**, a `ptr` a module gave you (a list, a map, a JSON document, a socket), is the program's to free, through its module, and `defer` keeps the free next to the `new`: @@ -279,9 +282,10 @@ main() { ### Common mistakes -- **Freeing what the compiler frees.** A string or a `make` buffer needs - no free, and freeing a parameter the function only borrows is refused - (E0901). Free handles. +- **Freeing what the compiler frees, or not freeing what it leaves.** A + string needs no free, nor does a `make` buffer a local holds; freeing a + parameter the function only borrows is refused (E0901). A buffer stored + in a struct's field is not freed with the struct: free it. - **Forgetting a handle.** A `list.new()` with no `list.free` leaks it. Write the `defer` on the next line. - **Changing a local's type.** `count = 0` then `count = "many"` is a @@ -395,11 +399,10 @@ main() { ## 2. Actors -Aether's concurrency is actors: a piece of state that only messages reach. -Nothing outside an actor touches its state; other code sends it a message, -and it changes its own state, one message at a time. So there are no locks -to take and no data races to find. The scheduler runs actors on every -core, so many actors use the whole machine. +Aether's concurrency is actors: a piece of state that messages reach. +Other code sends an actor a message, and the actor changes its own state, +one message at a time, so there are no locks to take. The scheduler runs +actors on every core, so many actors use the whole machine. ### A first actor, its state, and asking it @@ -619,7 +622,7 @@ got 6 [10-tcp](../examples/03-std/10-tcp.ae) · [11-cryptography](../examples/03-std/11-cryptography.ae) -The standard library follows the two rules of chapter 1 everywhere. A call +The standard library follows two rules from chapter 1 everywhere. A call that can fail returns its value and an error string, so a program checks `err` and goes on. What a module hands out as a `ptr` (a JSON document, a server, a socket) is a handle the program frees through that module. @@ -631,8 +634,9 @@ server, a socket) is a handle the program frees through that module. builds documents to encode. - **The network.** `std.http` serves by route and fetches: the same server API carries middleware, health probes and metrics, HTTP/2 and TLS, and - `std.http.proxy` forwards to upstreams. Under it, `std.tcp` is the - socket: a byte stream, so a protocol says where a message ends. + `std.http.proxy` forwards to upstreams. `std.tcp` is the socket itself, + the layer HTTP runs on: a byte stream, so a protocol says where a message + ends. - **Logs and secrets.** `std.log` writes levelled lines. The `std.cryptography` modules are written in Aether: hashes, HMACs, signatures, and the pure-Aether TLS. @@ -666,7 +670,7 @@ outcome. A deadline or a cancel lands at the call's next safe point, a loop head, so code with no loop (straight-line code, or a long C call) runs on until it returns. Aether also hosts other languages under the sandbox of chapter 5: Python, Lua and Tcl inside the program, a Go program as a -child process, and a TinyGo library in-process. [C +child process, and a Go library built as a C shared library in-process. [C interoperability](c-interop.md) and [emit=lib](emit-lib.md) are the references. diff --git a/examples/01-language/17-ownership.ae b/examples/01-language/17-ownership.ae index 4c30d662a..714a7ba43 100644 --- a/examples/01-language/17-ownership.ae +++ b/examples/01-language/17-ownership.ae @@ -13,9 +13,11 @@ // freeing a parameter it only borrows is refused (E0901). // // A slice's buffer, the one `make([]T, n)` hands out or `push` grows, has -// one owner instead of a count: the local that holds it, which releases it -// when its scope ends, or whatever it was moved to. The program writes no -// free for it either. +// one owner instead of a count: a local that holds it releases it when its +// scope ends, and so does a local, a caller or a `@keep` parameter it moves +// to. A buffer put in a field, a module `var`, a tuple or a message, or +// held by a local a closure captures, is the program's to give back: +// `free(s.items)` releases it and empties the field. // // Handles are `ptr`s, and a `ptr` owns nothing: a list or a map, a // `heap.new` box, a C allocation. Whoever makes one diff --git a/examples/02-actors/15-streams.ae b/examples/02-actors/15-streams.ae index 9d23a990c..92556726b 100644 --- a/examples/02-actors/15-streams.ae +++ b/examples/02-actors/15-streams.ae @@ -33,8 +33,8 @@ actor Producer { // Offer lines until the window is full or every line has gone. More() -> { while sent < total { - if stream.offer(s, "log line ${sent + 1}") == 0 { - break // no credit left: wait for the next one + if stream.offer(s, "log line ${sent + 1}") != 1 { + break // 0: no credit left, wait for the next one } sent = sent + 1 } diff --git a/examples/03-std/10-tcp.ae b/examples/03-std/10-tcp.ae index 5a51f7e0c..fd8058091 100644 --- a/examples/03-std/10-tcp.ae +++ b/examples/03-std/10-tcp.ae @@ -56,7 +56,9 @@ main() { client, cerr = tcp.connect("127.0.0.1", tcp.server_port(srv)) check("connect", cerr) defer tcp.close(client) - _sent, werr = tcp.write(client, "hello over tcp\n") + // write_all, because one write may send only part of what it is given. + request = "hello over tcp\n" + _sent, werr = tcp.write_all(client, string.bytes(request)) check("the client's write", werr) // The server takes the connection, reads the line, answers in capitals. @@ -67,7 +69,8 @@ main() { line, rerr = read_line(&server_side) check("the server's read", rerr) println("server: got '${line}'") - _answered, aw = tcp.write(conn, "${string.to_upper(line)}\n") + reply_line = "${string.to_upper(line)}\n" + _answered, aw = tcp.write_all(conn, string.bytes(reply_line)) check("the server's write", aw) // The client reads the answer. diff --git a/examples/README.md b/examples/README.md index 6c05babdc..cd4d10266 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,8 +15,8 @@ ae run examples/01-language/01-hello.ae With a toolchain built from this tree, that is `./build/ae run ...`. [The tutorial](../docs/tutorial.md) walks this path in prose, a chapter -per part below, with the mistakes newcomers make and exercises; the -[glossary](../docs/glossary.md) defines its words. The [language +per part below (the first two with the mistakes newcomers make and +exercises); the [glossary](../docs/glossary.md) defines its words. The [language reference](../docs/language-reference.md) is the full story behind each step; every row below links the part it builds on. [Getting started](../docs/getting-started.md) installs the toolchain. @@ -157,9 +157,9 @@ reversal is `input()[::-1]`), so the counts compare length, not like for like. | [03-fibonacci.ae](08-rosetta/03-fibonacci.ae) | [Fibonacci sequence](https://rosettacode.org/wiki/Fibonacci_sequence) | 22, 6, 10 | | [04-factorial.ae](08-rosetta/04-factorial.ae) | [Factorial](https://rosettacode.org/wiki/Factorial) | 12, 19, 2 | | [05-gcd.ae](08-rosetta/05-gcd.ae) | [Greatest common divisor](https://rosettacode.org/wiki/Greatest_common_divisor) | 9, 35, 1 | -| [06-sieve.ae](08-rosetta/06-sieve.ae) | [Sieve of Eratosthenes](https://rosettacode.org/wiki/Sieve_of_Eratosthenes) | 22, 33, 7 | +| [06-sieve.ae](08-rosetta/06-sieve.ae) | [Sieve of Eratosthenes](https://rosettacode.org/wiki/Sieve_of_Eratosthenes) | 21, 33, 7 | | [07-hanoi.ae](08-rosetta/07-hanoi.ae) | [Towers of Hanoi](https://rosettacode.org/wiki/Towers_of_Hanoi) | 9, 25, 6 | -| [08-n-queens.ae](08-rosetta/08-n-queens.ae) | [N-queens problem](https://rosettacode.org/wiki/N-queens_problem) | 50, 51, 7 | +| [08-n-queens.ae](08-rosetta/08-n-queens.ae) | [N-queens problem](https://rosettacode.org/wiki/N-queens_problem) | 48, 51, 7 | | [09-reverse-string.ae](08-rosetta/09-reverse-string.ae) | [Reverse a string](https://rosettacode.org/wiki/Reverse_a_string) | 13, 63, 1 | | [10-palindrome.ae](08-rosetta/10-palindrome.ae) | [Palindrome detection](https://rosettacode.org/wiki/Palindrome_detection) | 26, 11, 2 | | [11-roman-encode.ae](08-rosetta/11-roman-encode.ae) | [Roman numerals/Encode](https://rosettacode.org/wiki/Roman_numerals/Encode) | 17, 32, 2 | @@ -167,15 +167,15 @@ reversal is `input()[::-1]`), so the counts compare length, not like for like. | [14-quicksort.ae](08-rosetta/14-quicksort.ae) | [Sorting algorithms/Quicksort](https://rosettacode.org/wiki/Sorting_algorithms/Quicksort) | 36, 66, 19 | | [15-binary-search.ae](08-rosetta/15-binary-search.ae) | [Binary search](https://rosettacode.org/wiki/Binary_search) | 20, 12, 9 | | [16-hailstone.ae](08-rosetta/16-hailstone.ae) | [Hailstone sequence](https://rosettacode.org/wiki/Hailstone_sequence) | 23, 29, 14 | -| [19-levenshtein.ae](08-rosetta/19-levenshtein.ae) | [Levenshtein distance](https://rosettacode.org/wiki/Levenshtein_distance) | 27, 34, 17 | +| [19-levenshtein.ae](08-rosetta/19-levenshtein.ae) | [Levenshtein distance](https://rosettacode.org/wiki/Levenshtein_distance) | 26, 34, 17 | | [20-happy-numbers.ae](08-rosetta/20-happy-numbers.ae) | [Happy numbers](https://rosettacode.org/wiki/Happy_numbers) | 16, 26, 10 | | [21-perfect-numbers.ae](08-rosetta/21-perfect-numbers.ae) | [Perfect numbers](https://rosettacode.org/wiki/Perfect_numbers) | 18, 29, 6 | | [22-look-and-say.ae](08-rosetta/22-look-and-say.ae) | [Look-and-say sequence](https://rosettacode.org/wiki/Look-and-say_sequence) | 22, 28, 17 | -| [23-josephus.ae](08-rosetta/23-josephus.ae) | [Josephus problem](https://rosettacode.org/wiki/Josephus_problem) | 27, 39, 13 | +| [23-josephus.ae](08-rosetta/23-josephus.ae) | [Josephus problem](https://rosettacode.org/wiki/Josephus_problem) | 26, 39, 13 | | [24-pascals-triangle.ae](08-rosetta/24-pascals-triangle.ae) | [Pascal's triangle](https://rosettacode.org/wiki/Pascal's_triangle) | 20, 35, 9 | | [25-prime-decomposition.ae](08-rosetta/25-prime-decomposition.ae) | [Prime decomposition](https://rosettacode.org/wiki/Prime_decomposition) | 34, 35, 52 | -Over these 22 tasks: Aether 455 lines, Go 642, Python 233. Three tasks of +Over these 22 tasks: Aether 450 lines, Go 642, Python 233. Three tasks of the tranche wait for compiler fixes and are left out rather than written around them: Roman numerals/Decode and Leap year (#3478, an `if` value whose condition ends in a call) and Caesar cipher (#3479, `c = s[i]`). From 012c9a4cf8392b0e062d5fc2d6a891c55650e301 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 16:57:00 -0300 Subject: [PATCH 09/13] test: the ABI matrix shows an Aether consumer's stderr when it fails (#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. --- tests/integration/abi_matrix/test_abi_matrix.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integration/abi_matrix/test_abi_matrix.sh b/tests/integration/abi_matrix/test_abi_matrix.sh index 6a7e956c6..e3f4878f0 100644 --- a/tests/integration/abi_matrix/test_abi_matrix.sh +++ b/tests/integration/abi_matrix/test_abi_matrix.sh @@ -159,8 +159,13 @@ run_app() { bad "$1: app.ae did not build" "$2/app_build.log" return fi - expect "$1" "$(cd "$2" && PATH="$2:$PATH" LD_LIBRARY_PATH="$2${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ - DYLD_LIBRARY_PATH="$2${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" ./app$exe 2>"$2/app.err" | tr -d '\r')" "$app_want" + got="$(cd "$2" && PATH="$2:$PATH" LD_LIBRARY_PATH="$2${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + DYLD_LIBRARY_PATH="$2${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" ./app$exe 2>"$2/app.err" | tr -d '\r')" + expect "$1" "$got" "$app_want" + # What the program said on stderr: an abort prints nothing on stdout. + if [ "$got" != "$app_want" ] && [ -s "$2/app.err" ]; then + echo " stderr:"; tail -12 "$2/app.err" | sed 's/^/ /' + fi } run_app "this importer, the $old_version library" "$TMP/old" "$ROOT" "$AE" run_app "the $old_version importer, this library" "$TMP/new" "$OLD_HOME" "$OLD_AE" From 682c91706d7851d259a2a6b6a90389ffd8851d61 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 17:51:24 -0300 Subject: [PATCH 10/13] fix: a library with a runtime of its own keeps it on Linux and FreeBSD (#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. --- docs/abi-policy.md | 7 +- docs/design/ownership-model.md | 9 +- docs/emit-lib.md | 19 ++-- ...d-3481-library-keeps-its-runtime-on-elf.md | 15 +++ .../integration/abi_matrix/test_abi_matrix.sh | 39 ++++++- .../test_binary_import_actors.sh | 18 ++- .../test_binary_import_ownership.sh | 15 ++- .../binary_import_structs/app_panic.ae | 8 +- .../test_binary_import_structs.sh | 51 ++++----- .../test_sandbox_nested_libraries.sh | 28 ++--- tools/ae.c | 106 ++++++++++++++++-- 11 files changed, 221 insertions(+), 94 deletions(-) create mode 100644 new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md diff --git a/docs/abi-policy.md b/docs/abi-policy.md index 540bf1d03..edc84e41d 100644 --- a/docs/abi-policy.md +++ b/docs/abi-policy.md @@ -47,7 +47,10 @@ library). A newer minor whose duties it knows is read. **The runtime ABI.** `AETHER_RUNTIME_ABI_ID` (`runtime/aether_abi.h`) is the Aether version, `/abi` and `AETHER_RUNTIME_ABI_REV`, and the flags that change layouts (`+debug-mailbox`). A self-contained library carries -its own runtime, so this ABI never crosses its edge. A `--shared-runtime` +its own runtime, so this ABI never crosses its edge: on ELF `ae` links it +`-Bsymbolic`, so its runtime calls bind to its own copy, not to the one a +program linked `-rdynamic` exports, as macOS and Windows keep them apart by +themselves (#3481). A `--shared-runtime` library, and C emitted with `--emit=csrc` or `--emit=obj`, must run on the runtime it was made for. The shared runtime reports its ID (`aether_runtime_abi()`), and the catalog of a shared-runtime library @@ -87,7 +90,7 @@ catalogs and libraries compiled for a runtime of another ABI. | Combination | Supported | |---|---| | A C host and a self-contained library of any version | Yes, using the functions of the library's level (read `host_abi`); a host built with older headers uses its own level's | -| An Aether program importing a self-contained library | Yes, when the importer knows every duty the catalog requires; a library of an older version is read by its minor | +| An Aether program importing a self-contained library | Yes, when the importer knows every duty the catalog requires; a library of an older version is read by its minor. On ELF, a library of another runtime ABI linked before #3481 (without `-Bsymbolic`, so it would run on the program's runtime) is refused at import, naming the rebuild | | An importer older than schema 1.11 and a newer library | Calls that succeed work; it cannot read `requires`, so it does not check outcomes, and a failed call reads as its zero value, as with 0.856's own libraries | | Anything on `--shared-runtime` | Only on the runtime it was made for (one runtime ABI ID) | | Two self-contained libraries of different versions in one process | Yes; each runs on its own runtime, and the shared facilities are refused across layouts | diff --git a/docs/design/ownership-model.md b/docs/design/ownership-model.md index bdc3b3011..b47870531 100644 --- a/docs/design/ownership-model.md +++ b/docs/design/ownership-model.md @@ -901,10 +901,11 @@ struct holding a string has no such copy: by value, the program's drop releases the library's strings, and through a typed pointer the program's stores do. A builder that uses its config fills, grows or frees a map the program made, and a factory in the library would not help, since the -trailing block fills the config with the program's runtime. So where the -copies stay apart (macOS, Windows: no ELF interposition) `ae` refuses to -import a static-runtime library whose exports pass such a struct or whose -builders use their config, and names the rebuild with `--shared-runtime`. +trailing block fills the config with the program's runtime. So, since the +copies stay apart on every platform (#3481: a self-contained library binds +to its own runtime on ELF too), `ae` refuses to import a static-runtime +library whose exports pass such a struct or whose builders use their +config, and names the rebuild with `--shared-runtime`. Closures cross either way: an environment carries its own release, which runs in the library that made it. diff --git a/docs/emit-lib.md b/docs/emit-lib.md index b9ea3b2d7..28abff5e0 100644 --- a/docs/emit-lib.md +++ b/docs/emit-lib.md @@ -1254,11 +1254,13 @@ Scope of the current implementation: A library built the default way carries its own copy of the runtime, linked in statically: one scheduler, one set of panic frames, one config per image. That keeps a library self-contained for a C or Python host, but a program -that imports Aether libraries in binary form then runs on several runtimes. -On Linux and FreeBSD ELF symbol interposition happens to bind a library's -runtime calls to the program's copy; macOS's two-level namespace and Windows' -PE keep them apart, so a panic raised inside a library never reaches the -program's `catch` there. +that imports Aether libraries in binary form then runs on several runtimes, +on every platform: a panic raised inside a library never reaches the +program's `catch`. macOS's two-level namespace and Windows' PE keep the +copies apart by themselves; on ELF (Linux, FreeBSD) `ae` links a +self-contained library with `-Bsymbolic`, so its runtime calls bind to its +own copy, not to the one a program linked `-rdynamic` exports (#3481: they +did, and a library made for one release then ran on another's runtime). `--shared-runtime` links against the runtime as a shared library instead, `aether.dll` (with `libaether.dll.a`), `libaether.dylib` or `libaether.so`, @@ -1324,10 +1326,9 @@ ae build app.ae -o app # has none way out, `main()` drains the scheduler and joins it, so a message the library sent fire-and-forget is still delivered. - **Draining needs one runtime.** The drain reaches the library's actors - where the program and the library share one runtime: `--shared-runtime` - everywhere, or ELF interposition with the default static runtime (Linux, - FreeBSD). On macOS and Windows a static library's actors run on that - library's own scheduler. `ae` warns that messages still in flight when the + where the program and the library share one runtime: `--shared-runtime`. + A static library's actors run on that library's own scheduler, on every + platform (#3481). `ae` warns that messages still in flight when the program exits are not delivered, and names the library to rebuild with `--shared-runtime`. - **A C host** calls `scheduler_shutdown()` before it exits to deliver what diff --git a/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md new file mode 100644 index 000000000..2b653af0b --- /dev/null +++ b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md @@ -0,0 +1,15 @@ +- **A library with a runtime of its own keeps it on Linux and FreeBSD + (#3481).** An `ae build` program is linked `-rdynamic`, so it exports + its runtime, and on ELF a library's calls into the runtime it carries + bound to the program's instead: its code ran on a runtime it was not + made for. Across versions its runtime ABI check refused, and the program + ended on the panic ("the library was made for Aether runtime 0.860.0/abi1 + and runs on 0.863.0/abi1"), which kept the cross-version ABI matrix red + on Linux since 0.861.0. `ae` now links such a library `-Bsymbolic`, as + macOS and Windows keep the copies apart by themselves, so on every + platform a static-runtime library runs on its own runtime: `ae` refuses + an import that needs one runtime (a struct holding strings, a builder + using its config) and warns that a library's actors are not drained at + exit, naming `--shared-runtime`, as it did on macOS and Windows. A + library of another runtime ABI linked before this is refused at import + on ELF, naming the rebuild, instead of ending the program. diff --git a/tests/integration/abi_matrix/test_abi_matrix.sh b/tests/integration/abi_matrix/test_abi_matrix.sh index e3f4878f0..e38806574 100644 --- a/tests/integration/abi_matrix/test_abi_matrix.sh +++ b/tests/integration/abi_matrix/test_abi_matrix.sh @@ -13,7 +13,10 @@ # a panic in one, an account of the new one suspended while the old one # runs and resumed, both unloaded # Aether consumers (app.ae, a binary import): -# this importer, the old library; the old importer, this library +# this importer, the old library; the old importer, this library. On +# ELF this ae links a library -Bsymbolic, so it keeps its own runtime in +# a program that exports one (#3481); an old library linked without it, +# of another version, is refused at import, naming the rebuild. # The shared runtime: # this host, this library on this shared runtime the baseline # this host, the old library on the old shared runtime @@ -167,7 +170,39 @@ run_app() { echo " stderr:"; tail -12 "$2/app.err" | sed 's/^/ /' fi } -run_app "this importer, the $old_version library" "$TMP/old" "$ROOT" "$AE" +# A library with a runtime of its own linked before #3481 on ELF binds that +# runtime to the program's (an `ae build` program exports its runtime), so it +# would run on this one, of another version: this importer refuses it at +# build, naming the rebuild, instead of the program ending on its first call. +refuse_app() { + cp "$SCRIPT_DIR/app.ae" "$2/app.ae" + if (cd "$2" && AETHER_HOME="$3" "$4" build app.ae -o app > app_build.log 2>&1); then + bad "$1: app.ae built; the import was not refused" "$2/app_build.log" + elif ! grep -q "carries the Aether runtime" "$2/app_build.log" || + ! grep -q -- "--shared-runtime" "$2/app_build.log"; then + bad "$1: the refusal does not say why and name the rebuild" "$2/app_build.log" + fi +} +this_version="$(tr -d '\r\n' < "$ROOT/VERSION" 2>/dev/null)" +# This ae links a library with a runtime of its own -Bsymbolic on ELF, so it +# keeps that runtime in any program (#3481). +if [ "$ext" = so ] && command -v readelf >/dev/null 2>&1 && + ! readelf -d "$TMP/new/libmlib.$ext" 2>/dev/null | grep -q SYMBOLIC; then + bad "this ae linked libmlib.$ext without -Bsymbolic: it would run on an importing program's runtime" +fi +old_binds_own=1 +if [ "$ext" = so ] && [ "$old_version" != "$this_version" ]; then + if ! command -v readelf >/dev/null 2>&1; then + old_binds_own=unknown + elif ! readelf -d "$TMP/old/libmlib.$ext" 2>/dev/null | grep -q SYMBOLIC; then + old_binds_own=0 + fi +fi +case "$old_binds_own" in + 0) refuse_app "this importer, the $old_version library (linked before #3481)" "$TMP/old" "$ROOT" "$AE" ;; + unknown) echo " [NOTE] abi_matrix: no readelf, so this importer with the $old_version library is not run" ;; + *) run_app "this importer, the $old_version library" "$TMP/old" "$ROOT" "$AE" ;; +esac run_app "the $old_version importer, this library" "$TMP/new" "$OLD_HOME" "$OLD_AE" # The shared runtime. $1 = case dir, $2 = library dir, $3 = runtime dir, diff --git a/tests/integration/binary_import_actors/test_binary_import_actors.sh b/tests/integration/binary_import_actors/test_binary_import_actors.sh index 8aa1134ec..ecb0666d1 100644 --- a/tests/integration/binary_import_actors/test_binary_import_actors.sh +++ b/tests/integration/binary_import_actors/test_binary_import_actors.sh @@ -11,10 +11,9 @@ # way out: crowd's slow fire-and-forget message is still delivered. # # Two passes over the same sources: -# static runtime each library carries its own libaether. The actors run -# wherever the importer's calls land; the exit drain reaches -# them only where ELF interposition merges the copies -# (Linux, FreeBSD). Elsewhere `ae` warns that it cannot. +# static runtime each library carries its own libaether and runs its +# actors on its own scheduler, which the program's exit +# does not drain, on every platform (#3481); `ae` warns. # shared runtime `--shared-runtime`: one runtime, one scheduler, and the # program's exit drains it on every platform. set -e @@ -25,10 +24,9 @@ AE="$ROOT/build/ae" [ -x "$AE" ] || [ -x "$AE.exe" ] || { echo " [SKIP] binary_import_actors: ae not built"; exit 0; } case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; ELF=0; EXE=".exe" ;; - Darwin) SO_EXT=".dylib"; ELF=0; EXE="" ;; - Linux|FreeBSD) SO_EXT=".so"; ELF=1; EXE="" ;; - *) SO_EXT=".so"; ELF=0; EXE="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; EXE=".exe" ;; + Darwin) SO_EXT=".dylib"; EXE="" ;; + *) SO_EXT=".so"; EXE="" ;; esac fail() { @@ -65,7 +63,7 @@ run_pass() { echo "$OUT" | grep -q "^OK" || { echo "$OUT"; fail "$label: the actors did not count to 300"; } if [ "$drains" = 1 ]; then echo "$OUT" | grep -q "^late 7" || { echo "$OUT"; fail "$label: the exit did not drain the library's in-flight message" run.log; } - elif [ "$ELF" = 0 ]; then + else grep -q "runs actors on its own static runtime" run.log \ || fail "$label: no warning that the static runtime's messages are not drained" run.log fi @@ -80,7 +78,7 @@ run_pass() { } # ---- static runtime ---- -run_pass "$WORK/static" "" "static runtime" "$ELF" +run_pass "$WORK/static" "" "static runtime" 0 # ---- shared runtime (where this toolchain built one) ---- if [ -d "$ROOT/build/shared" ]; then diff --git a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh index 13500f894..990254191 100644 --- a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh +++ b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh @@ -11,9 +11,9 @@ # finds nothing on macOS. # 3. A struct holding a string, returned by value, and a builder that frees # its config. Neither can be copied between two copies of the runtime, so -# they need one: the shared runtime, or the static one where ELF -# interposition merges the copies (Linux, FreeBSD). There they run and -# grow 0; elsewhere the import is refused, naming the rebuild. +# they need one, the shared runtime: with the static one each library +# keeps its own copy (on every platform since #3481), and the import is +# refused, naming the rebuild. On the shared runtime they run and grow 0. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" @@ -22,10 +22,9 @@ AE="$ROOT/build/ae" CC="${CC:-cc}" case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; ELF=0; EXE=".exe" ;; - Darwin) SO_EXT=".dylib"; ELF=0; EXE="" ;; - Linux|FreeBSD) SO_EXT=".so"; ELF=1; EXE="" ;; - *) SO_EXT=".so"; ELF=0; EXE="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; EXE=".exe" ;; + Darwin) SO_EXT=".dylib"; EXE="" ;; + *) SO_EXT=".so"; EXE="" ;; esac LEAKS=0 if [ "$(uname -s)" = Darwin ] && command -v leaks >/dev/null 2>&1; then LEAKS=1; fi @@ -102,7 +101,7 @@ run_pass() { cd "$WORK" } -run_pass "$WORK/static" "" "static runtime" "$ELF" +run_pass "$WORK/static" "" "static runtime" 0 # The C host, against the static library. cd "$WORK/static" diff --git a/tests/integration/binary_import_structs/app_panic.ae b/tests/integration/binary_import_structs/app_panic.ae index 4b9c8d614..d0e8f6eff 100644 --- a/tests/integration/binary_import_structs/app_panic.ae +++ b/tests/integration/binary_import_structs/app_panic.ae @@ -1,8 +1,8 @@ // A panic inside a binary-imported library reaches the host's catch -// (#2297). Holds where the host and the library share one runtime: on ELF -// (Linux, FreeBSD) the library's runtime symbols bind to the host's by -// interposition. The test script runs this only there; macOS and Windows -// keep a runtime per image until #2297's shared-runtime work lands. +// (#2297). Holds where the host and the library share one runtime: a +// library built with --shared-runtime, which is how the test script runs +// it. A library with a runtime of its own keeps it on every platform +// (#3481), so its panic stays in it. import engine diff --git a/tests/integration/binary_import_structs/test_binary_import_structs.sh b/tests/integration/binary_import_structs/test_binary_import_structs.sh index e082d45ea..55394234d 100644 --- a/tests/integration/binary_import_structs/test_binary_import_structs.sh +++ b/tests/integration/binary_import_structs/test_binary_import_structs.sh @@ -10,14 +10,11 @@ # and a function taking or returning a struct was not exported at all. # # Two passes over the same sources: -# static runtime each library carries its own copy of libaether. Where -# ELF interposition merges the copies (Linux, FreeBSD), a -# second binary library (script) importing the engine, and -# the host importing both, share the engine's one module -# state, and a panic in the engine reaches the host's -# catch. Elsewhere the copies stay apart and neither may -# free the other's strings, so importing the engine, whose -# *Model holds one, is refused, naming the rebuild (#2931). +# static runtime each library carries its own copy of libaether, and on +# every platform (#3481) the copies stay apart: neither +# may free the other's strings, so importing the engine, +# whose *Model holds one, is refused, naming the rebuild +# (#2931). # shared runtime `--shared-runtime`: the libraries link aether.dll / # libaether.so / libaether.dylib, their catalogs say so # (schema 1.5), and the host follows. One runtime, so the @@ -30,10 +27,9 @@ AE="$ROOT/build/ae" [ -x "$AE" ] || [ -x "$AE.exe" ] || { echo " [SKIP] binary_import_structs: ae not built"; exit 0; } case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; ELF=0; EXE=".exe" ;; - Darwin) SO_EXT=".dylib"; ELF=0; EXE="" ;; - Linux|FreeBSD) SO_EXT=".so"; ELF=1; EXE="" ;; - *) SO_EXT=".so"; ELF=0; EXE="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; EXE=".exe" ;; + Darwin) SO_EXT=".dylib"; EXE="" ;; + *) SO_EXT=".so"; EXE="" ;; esac fail() { @@ -88,25 +84,20 @@ panic_check() { } # ---- static runtime ---- -if [ "$ELF" = 1 ]; then - run_pass "$WORK/static" "" "static runtime" - panic_check "$WORK/static" "static runtime" -else - mkdir -p "$WORK/static" - cp "$SCRIPT_DIR/engine.ae" "$SCRIPT_DIR/app.ae" "$WORK/static/" - cd "$WORK/static" - AETHER_HOME="$ROOT" "$AE" build --emit=lib engine.ae -o "libengine$SO_EXT" >engine.log 2>&1 \ - || fail "static runtime: ae build --emit=lib engine.ae" engine.log - rm -f engine.ae - if AETHER_HOME="$ROOT" "$AE" run app.ae >run.log 2>&1; then - fail "static runtime: importing a library whose *Model holds a string was not refused" run.log - fi - grep -q "model_new\` passes \*Model, which holds strings" run.log \ - || fail "static runtime: the refusal does not name model_new and *Model" run.log - grep -q "ae build --emit=lib --shared-runtime" run.log \ - || fail "static runtime: the refusal does not name the rebuild" run.log - cd "$WORK" +mkdir -p "$WORK/static" +cp "$SCRIPT_DIR/engine.ae" "$SCRIPT_DIR/app.ae" "$WORK/static/" +cd "$WORK/static" +AETHER_HOME="$ROOT" "$AE" build --emit=lib engine.ae -o "libengine$SO_EXT" >engine.log 2>&1 \ + || fail "static runtime: ae build --emit=lib engine.ae" engine.log +rm -f engine.ae +if AETHER_HOME="$ROOT" "$AE" run app.ae >run.log 2>&1; then + fail "static runtime: importing a library whose *Model holds a string was not refused" run.log fi +grep -q "model_new\` passes \*Model, which holds strings" run.log \ + || fail "static runtime: the refusal does not name model_new and *Model" run.log +grep -q "ae build --emit=lib --shared-runtime" run.log \ + || fail "static runtime: the refusal does not name the rebuild" run.log +cd "$WORK" # ---- shared runtime (where this toolchain built one) ---- if [ -d "$ROOT/build/shared" ]; then diff --git a/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh b/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh index e81c0bfda..ef5d426bb 100644 --- a/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh +++ b/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh @@ -12,10 +12,9 @@ # # 1. one runtime: both libraries' C linked into the host; # 2. each library built the default way, with its own copy of the runtime: -# on macOS and Windows the copies stay apart, so the inner one cannot read -# the outer block's grants; it refuses, and its message names -# --shared-runtime (on Linux and FreeBSD ELF interposition binds both to -# one copy: the same answers, no message); +# the copies stay apart (on every platform since #3481), so the inner one +# cannot read the outer block's grants; it refuses, and its message names +# --shared-runtime; # 2c. the outer library linked into a program with a runtime of its own # that exports nothing and looks first (#3445); # 2d. a program with the panic and no sandbox, -rdynamic on Linux, and a @@ -37,10 +36,10 @@ fail=0 want="before 1 inner 0 granted 0 outer 0 after 1" case "$(uname -s 2>/dev/null)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) ext=dll; apart=1; dl="" ;; - Darwin) ext=dylib; apart=1; dl="" ;; - Linux) ext=so; apart=0; dl="-ldl" ;; - *) ext=so; apart=0; dl="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) ext=dll; dl="" ;; + Darwin) ext=dylib; dl="" ;; + Linux) ext=so; dl="-ldl" ;; + *) ext=so; dl="" ;; esac # 1. one runtime @@ -95,7 +94,7 @@ if [ "$got" != "$want" ]; then head -5 "$TMP/apart.err" | sed 's/^/ /' fail=1 fi -if [ "$apart" = 1 ] && ! grep -q -- "--shared-runtime" "$TMP/apart.err"; then +if ! grep -q -- "--shared-runtime" "$TMP/apart.err"; then echo " [FAIL] sandbox_nested_libraries: a runtime per library: the refusal does not name --shared-runtime" head -5 "$TMP/apart.err" | sed 's/^/ /' fail=1 @@ -136,10 +135,11 @@ fi # 2d. a program with the panic of a runtime of its own and no sandbox, # linked -rdynamic on Linux as `ae build` links one, loading a library with a # runtime of its own whose enforce block catches a panic raised in a nested -# block (#3438): under ELF interposition the library runs the program's panic -# and try frames, so its sandbox must set the program's pointers. With them -# each copy's own, the catch left the nested level in force (PATH 0) and the -# block's end the outer one (probe 0 after it). +# block (#3438). Before #3481 the library ran the program's panic and try +# frames there (ELF interposition), so its sandbox had to set the program's +# pointers; now it runs its own on every platform, and the catch must still +# leave neither the nested level (PATH 0) nor, at the block's end, the outer +# one (probe 0 after it) in force. rdyn="" case "$(uname -s 2>/dev/null)" in Linux|FreeBSD) rdyn="-rdynamic" ;; esac # shellcheck disable=SC2046,SC2086 @@ -198,6 +198,6 @@ else fi if [ "$fail" = 0 ]; then - echo " [PASS] sandbox_nested_libraries: an inner library's export inside an outer enforce is held to it, its own grant_all narrows nothing, and the outer block stays checked: one runtime, a runtime per library (refused where the copies stay apart), a program whose copy looked first and exports nothing, a panic caught inside the block of a library that runs the program's panic$shared_note" + echo " [PASS] sandbox_nested_libraries: an inner library's export inside an outer enforce is held to it, its own grant_all narrows nothing, and the outer block stays checked: one runtime, a runtime per library (refused, naming --shared-runtime), a program whose copy looked first and exports nothing, a panic caught inside the block of a library loaded by a program linked -rdynamic$shared_note" fi exit $fail diff --git a/tools/ae.c b/tools/ae.c index 9d973b559..4bcb71e8a 100644 --- a/tools/ae.c +++ b/tools/ae.c @@ -498,6 +498,62 @@ static const char* const k_ae_import_duties[] = { #define AE_CATALOG_MINOR_KNOWN 11 +#if !defined(_WIN32) && !defined(__APPLE__) +/* An unsigned field of n bytes of an ELF file, in its byte order. */ +static uint64_t ae_elf_field(const unsigned char* p, int n, int big) { + uint64_t v = 0; + for (int i = 0; i < n; i++) v |= (uint64_t)p[big ? n - 1 - i : i] << (8 * i); + return v; +} + +/* #3481: 1 when the ELF library at `path` binds the definitions it holds to + * itself (DT_SYMBOLIC, or DF_SYMBOLIC in DT_FLAGS: linked -Bsymbolic, as + * `ae` links a library with a runtime of its own since #3481), 0 when it + * does not, -1 when it cannot be read. One that does not binds the runtime + * it carries to the program's when the program exports one (an `ae build` + * program is linked -rdynamic), and so runs on the program's runtime. Read + * from the file's headers and dynamic segment. */ +static int ae_elf_binds_own(const char* path) { + FILE* f = fopen(path, "rb"); + if (!f) return -1; + unsigned char eh[64]; + int r = -1; + size_t got = fread(eh, 1, sizeof(eh), f); + if (got >= 52 && memcmp(eh, "\x7f" "ELF", 4) == 0 && (eh[4] == 1 || eh[4] == 2)) { + int is64 = eh[4] == 2, big = eh[5] == 2; + uint64_t phoff = is64 ? ae_elf_field(eh + 32, 8, big) : ae_elf_field(eh + 28, 4, big); + int phentsize = (int)ae_elf_field(eh + (is64 ? 54 : 42), 2, big); + int phnum = (int)ae_elf_field(eh + (is64 ? 56 : 44), 2, big); + unsigned char ph[56]; + for (int i = 0; i < phnum && phentsize >= (is64 ? 56 : 32) && r < 0; i++) { + if (fseek(f, (long)(phoff + (uint64_t)i * (uint64_t)phentsize), SEEK_SET) != 0 || + fread(ph, 1, is64 ? 56 : 32, f) != (size_t)(is64 ? 56 : 32)) + break; + if (ae_elf_field(ph, 4, big) != 2) continue; /* PT_DYNAMIC */ + uint64_t off = is64 ? ae_elf_field(ph + 8, 8, big) : ae_elf_field(ph + 4, 4, big); + uint64_t size = is64 ? ae_elf_field(ph + 32, 8, big) : ae_elf_field(ph + 16, 4, big); + int ent = is64 ? 16 : 8; + if (size > (1u << 20) || fseek(f, (long)off, SEEK_SET) != 0) break; + unsigned char* dyn = (unsigned char*)malloc(size ? size : 1); + if (!dyn) break; + if (fread(dyn, 1, size, f) == size) { + r = 0; + for (uint64_t k = 0; k + (uint64_t)ent <= size; k += (uint64_t)ent) { + uint64_t tag = ae_elf_field(dyn + k, ent / 2, big); + uint64_t val = ae_elf_field(dyn + k + ent / 2, ent / 2, big); + if (tag == 0) break; /* DT_NULL */ + if (tag == 16) r = 1; /* DT_SYMBOLIC */ + if (tag == 30 && (val & 0x2)) r = 1; /* DT_FLAGS, DF_SYMBOLIC */ + } + } + free(dyn); + } + } + fclose(f); + return r; +} +#endif + /* A library's catalog against this importer (#2957): 0 when it can be used; * otherwise each reason on stderr, naming what is missing, both versions and * the fix, and -1. Called before anything of the library is used. */ @@ -543,6 +599,22 @@ static int ae_lib_check_compat(const _AeLibInfoMeta* m, const char* so_path) { so_path, m->runtime_abi ? m->runtime_abi : "(none)", AETHER_RUNTIME_ABI_ID); refused = 1; } +#if !defined(_WIN32) && !defined(__APPLE__) + /* #3481: a library with a runtime of its own, linked before `ae` bound + * one to itself on ELF, binds to the program's (an `ae build` program + * exports its runtime) and so runs on a runtime of another version than + * the one it was made for: its first call would refuse, and the program + * end on the panic. Refused here instead, naming the rebuild. */ + else if (!shared && minor >= 11 && m->runtime_abi && + strcmp(m->runtime_abi, AETHER_RUNTIME_ABI_ID) != 0 && ae_elf_binds_own(so_path) == 0) { + fprintf(stderr, "Error: %s carries the Aether runtime %s, but was linked by Aether %s to run on\n" + " the program's runtime instead, and this program's is %s.\n" + " Rebuild the library with this Aether (it then keeps its own runtime), or with\n" + " `ae build --emit=lib --shared-runtime`.\n", + so_path, m->runtime_abi, built_by, AETHER_RUNTIME_ABI_ID); + refused = 1; + } +#endif return refused ? -1 : 0; } @@ -5107,7 +5179,23 @@ void build_gcc_cmd(char* cmd, size_t size, * weak C main() for whoever links the OBJECT into an executable (see * emit_lib_weak_main in codegen.c); a shared library ae links itself has * no use for one, and should not export an entry point. */ + /* #3481: a library that carries its own runtime runs on it. On ELF a + * program linked -rdynamic exports its runtime, and the library's calls + * into its own copy bound to the program's instead (interposition): the + * library's code ran on a runtime it was not made for, of another + * version across releases, which its runtime ABI check refuses. + * -Bsymbolic binds a library's references to what it defines itself, + * as macOS's two-level namespace and Windows' PE always have; it still + * exports them, for a C host. A --shared-runtime library defines no + * runtime of its own, and keeps binding to the one it shares. */ +#if !defined(__APPLE__) + const char* emit_lib_flags = (g_emit_lib && !g_emit_exe) + ? (g_shared_runtime ? "-fPIC -shared -DAETHER_NO_LIB_MAIN " + : "-fPIC -shared -Wl,-Bsymbolic -DAETHER_NO_LIB_MAIN ") + : ""; +#else const char* emit_lib_flags = (g_emit_lib && !g_emit_exe) ? "-fPIC -shared -DAETHER_NO_LIB_MAIN " : ""; +#endif // Coverage builds skip -pipe — gcov works fine with it, but it // adds nothing when -O0 -g is already forced. Keeping the flag // string short helps the cmd-buffer size budget. @@ -5753,11 +5841,11 @@ static const char* ae_signature_string_struct(const _AeLibInfoMeta* m, int minor * values. A string crosses as a copy; a struct holding strings (by value or * through a typed pointer, where the program's stores release the * library's strings) and a builder's config the library uses have no copy, - * so where the library keeps its own runtime (no ELF interposition) such an - * import is refused, naming the rebuild that gives both one runtime. */ + * so a library that keeps its own runtime (every platform since #3481: on + * ELF it no longer binds to the program's) has such an import refused, + * naming the rebuild that gives both one runtime. */ static void ae_refuse_split_ownership(const _AeLibInfoMeta* m, int minor, const char* module, const char* so_path) { -#if defined(_WIN32) || defined(__APPLE__) char ty[200]; int refused = 0; for (int i = 0; i < m->function_count && m->functions; i++) { @@ -5799,9 +5887,6 @@ static void ae_refuse_split_ownership(const _AeLibInfoMeta* m, int minor, const "on the program's runtime.\n"); exit(1); } -#else - (void)m; (void)minor; (void)module; (void)so_path; -#endif } /* `module` NULL: the whole catalog (a bare import of a one-module @@ -6492,10 +6577,10 @@ static void prepare_binary_imports(const char* main_file) { g_binimport_static_names, g_binimport_static_rt > 1 ? "were" : "was", g_binimport_static_rt > 1 ? "them" : "it"); } -#if defined(_WIN32) || defined(__APPLE__) - /* Without ELF interposition, a static-runtime library's actors run on - * the library's own scheduler, which the program's exit does not drain: - * a message still in flight when main() returns is not delivered. */ + /* A static-runtime library's actors run on the library's own scheduler + * (on every platform since #3481), which the program's exit does not + * drain: a message still in flight when main() returns is not + * delivered. */ if (g_binimport_static_actor_names[0]) { fprintf(stderr, "Warning: %s runs actors on its own static runtime, so messages still in flight\n" @@ -6503,7 +6588,6 @@ static void prepare_binary_imports(const char* main_file) { " `ae build --emit=lib --shared-runtime` to run them on the program's.\n", g_binimport_static_actor_names); } -#endif } /* #2297: Windows has no rpath. After a link that names DLLs (binary imports, From b65fb05326ab79f0223d413afd47585674ea6610 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 18:25:16 -0300 Subject: [PATCH 11/13] fix: what the review of #3481's fix found (#3481) 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. --- Makefile | 4 +- compiler/codegen/codegen.c | 30 +++++++++++++- docs/build-system.md | 2 +- docs/containment-sandbox.md | 2 +- docs/emit-lib.md | 24 ++++++----- docs/stdlib-reference.md | 2 +- ...d-3481-library-keeps-its-runtime-on-elf.md | 24 ++++++----- runtime/aether_sandbox.h | 5 ++- .../script_gateway/aether_script_gateway.c | 5 ++- .../script_gateway/aether_script_gateway.h | 13 +++--- std/http/script_gateway/module.ae | 9 +++-- .../integration/abi_matrix/test_abi_matrix.sh | 9 +++-- .../binary_import_structs/app_panic.ae | 7 ++-- .../integration/emit_lib_failed_result/app.ae | 9 +++-- .../test_sandbox_nested_libraries.sh | 40 ++++++++++++++++++- tools/ae.c | 28 +++++++++---- tools/ae_cross.c | 7 +++- 17 files changed, 159 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index 9767e0679..e3dfd2e7c 100644 --- a/Makefile +++ b/Makefile @@ -2001,8 +2001,8 @@ docs-serve: docs docs-server # program and the --emit=lib libraries it imports in binary form share ONE # runtime: one scheduler, one set of panic frames (a panic raised in a library # reaches the program's catch), one config. A statically linked library keeps -# its own copy; ELF interposition happens to merge most of it on Linux, macOS -# and Windows merge none. `ae build --shared-runtime` links against this. +# its own copy on every platform (on ELF `ae` links it -Bsymbolic, #3481). +# `ae build --shared-runtime` links against this. # # It lives in its own directory, never beside libaether.a: a linker searching # -L for -laether takes a .so/.dylib/.dll.a there over the .a, and every diff --git a/compiler/codegen/codegen.c b/compiler/codegen/codegen.c index 706dfa9ce..ddf639cbc 100644 --- a/compiler/codegen/codegen.c +++ b/compiler/codegen/codegen.c @@ -4916,6 +4916,31 @@ static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { rs.result->kind == TYPE_STRING) strings_returned = 1; } int lib_runs_actors = gen->actor_count > 0 || gen->lib_actors; + /* #3481: a library with a runtime of its own keeps it on every platform + * (on ELF `ae` links it -Bsymbolic), so what needs one runtime has to + * meet an importer that knows: a struct holding strings across its edge, + * a builder that uses the config the program makes (both refused), and + * actors the program's exit cannot drain (warned of). An importer of + * 0.864.0 or before took the copies on ELF as merged and let them pass, + * one runtime then releasing the other's strings; naming the duty makes + * it refuse the library instead. */ + int needs_own_runtime = 0; + if (!gen->lib_shared_runtime) { + if (lib_runs_actors) needs_own_runtime = 1; + for (int i = 0; i < fn_count && !needs_own_runtime; i++) { + LibSig os; + const char* owhy = NULL; + if (!lib_fn_signature(gen, fns[i], &os, &owhy)) continue; + for (int k = -1; k < os.count && !needs_own_runtime; k++) { + Type* t = k < 0 ? os.result : os.ptypes[k]; + while (t && t->kind == TYPE_PTR && t->element_type) t = t->element_type; + if (struct_owning_strings(gen, t)) needs_own_runtime = 1; + } + } + for (int r = 0; r < clo_count && !needs_own_runtime; r++) + if (strcmp(rec_role[r], "builder") == 0 && rec_fn[r] && !builder_never_uses_config(rec_fn[r])) + needs_own_runtime = 1; + } /* #2957: "1.11" adds host_abi, runtime_abi and requires, the duties an * importer must perform to use this library, each one it knows by name; * one it does not know makes it refuse the library. The 1.10 duty is @@ -4925,10 +4950,11 @@ static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { * has the program run the scheduler. */ const char* schema = "1.11"; char requires[160]; - snprintf(requires, sizeof(requires), "outcome_check%s%s%s", + snprintf(requires, sizeof(requires), "outcome_check%s%s%s%s", strings_returned ? ",string_release" : "", gen->lib_shared_runtime ? ",shared_runtime,runtime_abi" : "", - lib_runs_actors ? ",actors" : ""); + lib_runs_actors ? ",actors" : "", + needs_own_runtime ? ",own_runtime" : ""); fprintf(gen->output, " \"%s\", \"", schema); fprintf(gen->output, "%s", AETHER_VERSION); /* the aetherc that wrote it */ fprintf(gen->output, "\", "); diff --git a/docs/build-system.md b/docs/build-system.md index 950b3e12c..ca03ce6aa 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -831,7 +831,7 @@ an executable link is the whole increment. | target | artifact | extra link flags | |---|---|---| -| `*-linux`, other ELF | `.so` | `-shared -fPIC` | +| `*-linux`, other ELF | `.so` | `-shared -fPIC -Wl,-Bsymbolic` (the library keeps the runtime it carries, #3481) | | `*-windows` | `.dll` | `-shared -fPIC -Wl,--export-all-symbols` | | `*-macos`, `*-ios` | `.dylib` | `-dynamiclib -install_name @rpath/` | | `wasm32-*` | `.wasm` | `--no-entry --gc-sections` + `--export=` per symbol | diff --git a/docs/containment-sandbox.md b/docs/containment-sandbox.md index a82e454c6..998a02e09 100644 --- a/docs/containment-sandbox.md +++ b/docs/containment-sandbox.md @@ -242,7 +242,7 @@ runtime (#2955): | two threads, each in its own block | each is checked against its own levels only; one leaving its block changes nothing for the other | | a message sent from a block | handled with no grants: an actor is not the block. That holds when the send steps an idle receiver of the same core inline, on the block's own thread: the step puts the block's levels aside and the block has them again when the send returns (#3393) | | a library's export called from inside a block on the same thread (through a host service, say), one runtime | held to the block's grants; its own `enforce` only narrows them, and when it ends the block is checked as before | -| the same, with libraries built without `--shared-runtime` on macOS or Windows | each library has a copy of the runtime of its own, which cannot read another copy's grants: the inner library's checked operations are refused, and the first refusal prints a message naming `--shared-runtime`. Every copy finds the others by the record it carries in its image (a PE section, an ELF note, a Mach-O section), in load order, the program first: libraries a host loads, `RTLD_LOCAL` ones included, and a program with the libraries it loads, whenever it loads them and though its executable exports nothing (Windows, or Linux without `-rdynamic`, #3445: its copy went unfound, and the library's code inside its deny-all block was allowed everything) | +| the same, with libraries built without `--shared-runtime` (on every platform since #3481; a library linked before it, without `-Bsymbolic`, binds to the program's runtime on Linux and FreeBSD) | each library has a copy of the runtime of its own, which cannot read another copy's grants: the inner library's checked operations are refused, and the first refusal prints a message naming `--shared-runtime`. Every copy finds the others by the record it carries in its image (a PE section, an ELF note, a Mach-O section), in load order, the program first: libraries a host loads, `RTLD_LOCAL` ones included, and a program with the libraries it loads, whenever it loads them and though its executable exports nothing (Windows, or Linux without `-rdynamic`, #3445: its copy went unfound, and the library's code inside its deny-all block was allowed everything) | | a contrib host bridge (`lua.run_sandboxed(perms, code)` and the others) | pushes `perms` as a level of the thread, so inside an `enforce` the guest gets the intersection | | a panic out of a block, caught anywhere (a `try` in any code, an actor's barrier) | the levels pushed inside the catching frame end with it, so the catch is held only to the blocks still around it | | more than 64 levels on one thread (blocks, trusted calls, bridge calls) | the 65th is refused with a panic before its block runs | diff --git a/docs/emit-lib.md b/docs/emit-lib.md index 28abff5e0..cf11bb4ef 100644 --- a/docs/emit-lib.md +++ b/docs/emit-lib.md @@ -1224,8 +1224,9 @@ function's `(..., _builder)` entry point). The stub is dropped onto the module search path, so the *existing* source-import machinery (typecheck, namespace prefixing, builder registration) rehydrates the library, no special call-site syntax. The artifact is linked in -(absolute path + `-rpath`), and the host's `-rdynamic` + static -`libaether` satisfy the `.so`'s runtime symbols. +(absolute path + `-rpath`), and the `.so` runs on the runtime it carries +(linked `-Bsymbolic` on ELF, #3481), or on the shared runtime when it was +built with `--shared-runtime`. Scope of the current implementation: @@ -1255,8 +1256,10 @@ A library built the default way carries its own copy of the runtime, linked in statically: one scheduler, one set of panic frames, one config per image. That keeps a library self-contained for a C or Python host, but a program that imports Aether libraries in binary form then runs on several runtimes, -on every platform: a panic raised inside a library never reaches the -program's `catch`. macOS's two-level namespace and Windows' PE keep the +on every platform. A panic raised inside a library ends the call in the +library's own execution frame, and the program's import stub raises the +failure again with the same reason (#2956), so a `try` around the call +catches it; the panic itself does not cross. macOS's two-level namespace and Windows' PE keep the copies apart by themselves; on ELF (Linux, FreeBSD) `ae` links a self-contained library with `-Bsymbolic`, so its runtime calls bind to its own copy, not to the one a program linked `-rdynamic` exports (#3481: they @@ -1290,9 +1293,12 @@ ae build app.ae -o app # imports both - Each copy of the runtime frees only what it made. A string an export returns crosses as a copy ([above](#strings-an-export-returns)), but a struct holding a string (returned by value, or reached through a typed - pointer) and a builder that uses its config have none. Where the copies - stay apart (macOS, Windows), importing a static library that has such an - export is an error naming each one, with the rebuild: `--shared-runtime`. + pointer) and a builder that uses its config have none. The copies stay + apart on every platform (#3481), so importing a static library that has + such an export is an error naming each one, with the rebuild: + `--shared-runtime`. Such a library names the `own_runtime` duty in its + catalog, so an importer of 0.864.0 or before, which took ELF's copies as + merged, refuses it rather than let the copies free each other's values. - A sandbox's grants are each thread's, in the runtime: on one runtime a library's export called from inside another's `sandbox.enforce` block is held to the block's grants. Where the copies stay apart, the inner copy @@ -1460,10 +1466,10 @@ The integration suite under `tests/integration/` covers: | `emit_lib_with_capability/` | `--with=fs,net,os` opt-ins; `--with=first-party` and `--with=all` aliases | | `lib_meta/` | `aether_lib_meta` + `ae lib-info` round-trip, schema, source, function count, three signatures, c_symbol gating, source refs | | `binary_import_package/` | `--package` builds two modules into one library: stable `aether___` symbols, schema 1.4 module records, export-list and `_`-suffix privacy, a host importing both modules by name from the binary (#2297) | -| `binary_import_structs/` | Schema 1.3 struct records and source signatures: a struct by value, `*Struct` field access, a function-pointer field, closures both ways with their captures, one module state shared by a host and a second binary library; static runtime on Linux/FreeBSD (a library panic caught; elsewhere the import is refused, since `*Model` holds a string) and `--shared-runtime` (caught everywhere, schema 1.5), Windows included (#2297) | +| `binary_import_structs/` | Schema 1.3 struct records and source signatures: a struct by value, `*Struct` field access, a function-pointer field, closures both ways with their captures, one module state shared by a host and a second binary library; static runtime (the import refused on every platform, since `*Model` holds a string, #3481) and `--shared-runtime` (a library panic caught everywhere, schema 1.5), Windows included (#2297) | | `binary_import_ownership/` | Strings an export or a builder returns are given back to the library's runtime: 0 growth and no leaks(1) findings for a binary import (static and shared runtime) and a C host releasing through the catalog's `string_release`; a struct holding a string and a builder freeing its config run on one runtime and are refused across two (#2931, #2861) | | `binary_import_keep/` | A `@keep ptr` and an `@into ptr` export cross the catalog's source signature, and each importer gets the same E0902 verdict from `ae check` against the library as against its source (a string or a borrowed `ptr` to the `@keep` parameter, an `@into` result that escapes); the accepted one runs (#3013) | -| `binary_import_actors/` | A library's actors in a program with none of its own: the first spawn starts the scheduler, schema 1.7 `actors` passes through a second library, and the program's exit drains a fire-and-forget message (shared runtime everywhere, static on Linux/FreeBSD; elsewhere a warning) (#2297) | +| `binary_import_actors/` | A library's actors in a program with none of its own: the first spawn starts the scheduler, schema 1.7 `actors` passes through a second library, and the program's exit drains a fire-and-forget message (shared runtime everywhere, static: a warning, on every platform since #3481) (#2297) | | `emit_lib_keeps_main/` | A program's `main()` as `aether_main` / `aether_main_exit`: a C host runs it (args, an ask/reply, its return value), the actors outlive `aether_main` and `aether_main_exit` drains them, a second `aether_main` is rejected, a rerun works without leaking scheduler tables (macOS `leaks`), the executable prints the same lines, no pair without `main()`, the reserved `main_exit`, the `--emit=csrc` header, and aarch64-linux / Android cross builds | | `emit_lib_kind_safe/` | Kind-discriminator predicates + deep-free safety, adversarial low-address probe (`(AetherValue*)42`) survives, kind correctly classifies map/list/scalar slots, deep-free walks nested map+list+scalars, magic-clear-on-free defends UAF probes | diff --git a/docs/stdlib-reference.md b/docs/stdlib-reference.md index 408791b5b..ee9ca2f8c 100644 --- a/docs/stdlib-reference.md +++ b/docs/stdlib-reference.md @@ -1398,7 +1398,7 @@ main() { Now `GET /greet/anything` runs `aether_script_handle` from `greeting.so` directly on the connection thread. -On Windows the script is a DLL loaded with `LoadLibrary`. A POSIX host links with `-rdynamic`, so a script's runtime calls bind to the host's own runtime; Windows has no equivalent, so the host and each script are built on the shared runtime (`aether.dll`) and run on it together: +On Linux, FreeBSD and macOS a script built the default way carries a runtime of its own and runs on it (#3481); built with `--shared-runtime`, it shares the host's. On Windows the script is a DLL loaded with `LoadLibrary`, which cannot bind to the host executable's runtime, so the host and each script are built on the shared runtime (`aether.dll`) and run on it together: ```sh ae build --emit=lib --shared-runtime --with=net greeting.ae -o greeting.dll diff --git a/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md index bdb47bbf6..b7b8852eb 100644 --- a/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md +++ b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md @@ -5,13 +5,17 @@ made for. Across versions its runtime ABI check refused, and the program ended on the panic ("the library was made for Aether runtime 0.860.0/abi1 and runs on 0.863.0/abi1"), which kept the cross-version ABI matrix red - on Linux since 0.861.0. `ae` now links such a library `-Bsymbolic`, as - macOS and Windows keep the copies apart by themselves, so on every - platform a static-runtime library runs on its own runtime: `ae` refuses - an import that needs one runtime (a struct holding strings, a builder - using its config) and warns that a library's actors are not drained at - exit, naming `--shared-runtime`, as it did on macOS and Windows. A - library of another runtime ABI linked before this is refused at import - on ELF, naming the rebuild, instead of ending the program. - This replaces 0.864.0's note that the matrix expects ELF's merged - runtimes: the copies no longer merge. + on Linux since 0.861.0. `ae` now links such a library `-Bsymbolic`, + natively and in cross builds, as macOS and Windows keep the copies apart + by themselves, so on every platform a static-runtime library runs on its + own runtime: `ae` refuses an import that needs one runtime (a struct + holding strings, a builder using its config) and warns that a library's + actors are not drained at exit, naming `--shared-runtime`, as it did on + macOS and Windows. Such a library names the `own_runtime` duty in its + catalog, so an importer of 0.864.0 or before, which took ELF's copies as + merged, refuses it instead of letting the two runtimes free each other's + values. A library of another runtime ABI linked before this is refused + at import on ELF, naming the rebuild, instead of ending the program. A + failure inside a library still reaches a `try` around the call: the + import stub raises it again (#2956). This replaces 0.864.0's note that + the matrix expects ELF's merged runtimes. diff --git a/runtime/aether_sandbox.h b/runtime/aether_sandbox.h index 3ec9a0685..60c284616 100644 --- a/runtime/aether_sandbox.h +++ b/runtime/aether_sandbox.h @@ -10,8 +10,9 @@ // // Generated code reaches all of this through the functions below, never by // naming the per-thread state (a Windows DLL cannot export thread-locals), -// so every library sharing one runtime image (the shared runtime, or ELF -// interposition) shares each thread's levels. +// so every library sharing one runtime image (the shared runtime, or, on +// ELF, a library linked without -Bsymbolic, which `ae` no longer makes since +// #3481) shares each thread's levels. // // When AETHER_HAS_SANDBOX is not defined, aether_sandbox_check is a // no-op macro that always returns 1, zero overhead. diff --git a/std/http/script_gateway/aether_script_gateway.c b/std/http/script_gateway/aether_script_gateway.c index c269965b4..d39210765 100644 --- a/std/http/script_gateway/aether_script_gateway.c +++ b/std/http/script_gateway/aether_script_gateway.c @@ -89,8 +89,9 @@ static int sg_readable(const char* path) { #ifdef _WIN32 /* A POSIX host links with -rdynamic, so a script built with its runtime - * calls left unresolved binds them to the host's own runtime at dlopen. - * Windows has nothing like it: a DLL's imports name the module that + * calls left unresolved (no runtime of its own) binds them to the host's + * runtime at dlopen; one built the default way keeps the runtime it carries + * (#3481). Windows has nothing like it: a DLL's imports name the module that * provides them, and a script cannot import from the host executable. Host * and script share one runtime there only when both link the shared runtime * (`ae build --shared-runtime`, aether.dll), which is what this checks. diff --git a/std/http/script_gateway/aether_script_gateway.h b/std/http/script_gateway/aether_script_gateway.h index 85cc7cb95..1c6a679db 100644 --- a/std/http/script_gateway/aether_script_gateway.h +++ b/std/http/script_gateway/aether_script_gateway.h @@ -15,11 +15,14 @@ * with RTLD_LOCAL so symbols don't pollute the host process's * global namespace. * - * On Windows the library is a DLL loaded with LoadLibrary, and the host - * and the script must both be built with `ae build --shared-runtime`: - * there is no -rdynamic there for a script to bind to the host's runtime, - * so they share aether.dll instead. A mismatch is refused at mount with - * KIND_IO (sg_host_runtime_check and sg_script_runtime_check in the .c). + * On POSIX a script built the default way carries a runtime of its own and + * runs on it (on Linux and FreeBSD too since #3481, where `ae` links it + * -Bsymbolic); one built with --shared-runtime shares the host's. On + * Windows the library is a DLL loaded with LoadLibrary, and the host and + * the script must both be built with `ae build --shared-runtime`: a DLL + * cannot bind to the host executable's runtime, so they share aether.dll + * instead. A mismatch is refused at mount with KIND_IO + * (sg_host_runtime_check and sg_script_runtime_check in the .c). * * Lifetime: dlopen handles are intentionally not dlclose()d. The * gateway is a long-lived hosting feature; the .so stays mapped diff --git a/std/http/script_gateway/module.ae b/std/http/script_gateway/module.ae index 922b53bb9..191d136cd 100644 --- a/std/http/script_gateway/module.ae +++ b/std/http/script_gateway/module.ae @@ -53,10 +53,11 @@ // // ### Windows // -// The library is a DLL, loaded with LoadLibrary. A POSIX host links with -// -rdynamic, so a script's runtime calls bind to the host's own runtime when -// it is loaded; Windows has no equivalent, so there the host and every -// script are built on the shared runtime (aether.dll) and run on it +// The library is a DLL, loaded with LoadLibrary. On Linux, FreeBSD and +// macOS a script built the default way carries a runtime of its own and runs +// on it (#3481); one built with --shared-runtime shares the host's. Windows +// has no way for a DLL to bind to the host executable, so there the host and +// every script are built on the shared runtime (aether.dll) and run on it // together: // // ```sh diff --git a/tests/integration/abi_matrix/test_abi_matrix.sh b/tests/integration/abi_matrix/test_abi_matrix.sh index 4db708f72..576f9be63 100644 --- a/tests/integration/abi_matrix/test_abi_matrix.sh +++ b/tests/integration/abi_matrix/test_abi_matrix.sh @@ -188,9 +188,12 @@ refuse_app() { bad "$1: the refusal does not say why and name the rebuild" "$2/app_build.log" fi } -if [ "$ext" = so ] && command -v readelf >/dev/null 2>&1 && - ! readelf -d "$TMP/new/libmlib.$ext" 2>/dev/null | grep -q SYMBOLIC; then - bad "this ae linked libmlib.$ext without -Bsymbolic: it would run on an importing program's runtime" +if [ "$ext" = so ]; then + if ! command -v readelf >/dev/null 2>&1; then + echo " [NOTE] abi_matrix: no readelf, so this ae's -Bsymbolic link is not checked" + elif ! readelf -d "$TMP/new/libmlib.$ext" 2>/dev/null | grep -q SYMBOLIC; then + bad "this ae linked libmlib.$ext without -Bsymbolic: it would run on an importing program's runtime" + fi fi old_static_abi="$(static_abi "$TMP/old/libmlib.$ext")" old_binds_own=1 diff --git a/tests/integration/binary_import_structs/app_panic.ae b/tests/integration/binary_import_structs/app_panic.ae index d0e8f6eff..ccfb980c6 100644 --- a/tests/integration/binary_import_structs/app_panic.ae +++ b/tests/integration/binary_import_structs/app_panic.ae @@ -1,8 +1,9 @@ // A panic inside a binary-imported library reaches the host's catch -// (#2297). Holds where the host and the library share one runtime: a +// (#2297) as itself where the host and the library share one runtime: a // library built with --shared-runtime, which is how the test script runs -// it. A library with a runtime of its own keeps it on every platform -// (#3481), so its panic stays in it. +// it. A library with a runtime of its own (on every platform since #3481) +// ends the call in its own frame, and the import stub raises the failure +// again in the program with the same reason (#2956). import engine diff --git a/tests/integration/emit_lib_failed_result/app.ae b/tests/integration/emit_lib_failed_result/app.ae index 766217379..96953f3d8 100644 --- a/tests/integration/emit_lib_failed_result/app.ae +++ b/tests/integration/emit_lib_failed_result/app.ae @@ -2,10 +2,11 @@ // runtime (#3388): a failed call is raised again as a panic here, and the // string the library would have returned is not kept by either runtime. // -// The reason is printed in the handler and not kept: on ELF, where the -// program is linked with -rdynamic, the library's runtime calls bind to the -// program's runtime (one runtime, not two), so words.used() reads the -// program's count, which a kept reason string would be part of (#3437). +// The reason is printed in the handler and not kept: a library linked +// without -Bsymbolic (before #3481) binds its runtime calls to a program +// linked -rdynamic on ELF (one runtime, not two), so words.used() would +// read the program's count, which a kept reason string would be part of +// (#3437). Printed and dropped, the count is the same either way. import words main() { diff --git a/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh b/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh index ef5d426bb..e73cf2454 100644 --- a/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh +++ b/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh @@ -18,7 +18,9 @@ # 2c. the outer library linked into a program with a runtime of its own # that exports nothing and looks first (#3445); # 2d. a program with the panic and no sandbox, -rdynamic on Linux, and a -# library whose block catches a panic from a nested block (#3438); +# library whose block catches a panic from a nested block (#3438); 2e, +# with a second library; 2f, on ELF, both linked without -Bsymbolic, so +# they run the program's panic (interposition); # 3. both built with --shared-runtime (skipped where build/shared has none). SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -171,6 +173,40 @@ if [ "$got" != "$want_two" ]; then head -5 "$TMP/caught2.err" | sed 's/^/ /' fail=1 fi +# 2f. on ELF, the same two libraries linked by hand from aetherc's C without +# -Bsymbolic, as a C build of `--emit=lib` C still may be and as libraries +# linked before #3481 were: under the program linked -rdynamic they run the +# program's panic, try frames and step barrier (interposition), so the hooks +# list of #3438 must serve each copy's levels. +hand_note="" +if [ -n "$rdyn" ]; then + mkdir -p "$TMP/hand" + hand_lib() { # $1 = source stem + "$ROOT/build/aetherc" --emit=lib --with=os,extern "$SCRIPT_DIR/$1.ae" "$TMP/hand/$1.c" \ + > "$TMP/hand/$1.log" 2>&1 && + # shellcheck disable=SC2046,SC2086 + "$CC" -O1 -shared -fPIC -DAETHER_NO_LIB_MAIN $("$AE" cflags --cflags) "$TMP/hand/$1.c" \ + "$SCRIPT_DIR/hooks.c" -o "$TMP/hand/lib$1.$ext" $("$AE" cflags --libs) $dl \ + >> "$TMP/hand/$1.log" 2>&1 + } + if ! hand_lib caught || ! hand_lib inner; then + echo " [FAIL] sandbox_nested_libraries: the hand-linked libraries did not build" + tail -8 "$TMP/hand/caught.log" "$TMP/hand/inner.log" 2>/dev/null | sed 's/^/ /' + fail=1 + elif command -v readelf >/dev/null 2>&1 && readelf -d "$TMP/hand/libcaught.$ext" | grep -q SYMBOLIC; then + echo " [FAIL] sandbox_nested_libraries: the hand-linked library is -Bsymbolic, so it tests nothing" + fail=1 + else + got="$("$TMP/host_caught" "$TMP/hand/libcaught.$ext" "$TMP/hand/libinner.$ext" 2>"$TMP/hand.err" | tr -d '\r')" + if [ "$got" != "$want_two" ]; then + echo " [FAIL] sandbox_nested_libraries: two hand-linked libraries running the program's panic:" + printf '%s\n' "$got" | sed 's/^/ got: /' + head -5 "$TMP/hand.err" | sed 's/^/ /' + fail=1 + fi + hand_note=", the same libraries linked without -Bsymbolic under it" + fi +fi # 3. one shared runtime if ls "$ROOT/build/shared/" 2>/dev/null | grep -q "aether"; then @@ -198,6 +234,6 @@ else fi if [ "$fail" = 0 ]; then - echo " [PASS] sandbox_nested_libraries: an inner library's export inside an outer enforce is held to it, its own grant_all narrows nothing, and the outer block stays checked: one runtime, a runtime per library (refused, naming --shared-runtime), a program whose copy looked first and exports nothing, a panic caught inside the block of a library loaded by a program linked -rdynamic$shared_note" + echo " [PASS] sandbox_nested_libraries: an inner library's export inside an outer enforce is held to it, its own grant_all narrows nothing, and the outer block stays checked: one runtime, a runtime per library (refused, naming --shared-runtime), a program whose copy looked first and exports nothing, a panic caught inside the block of a library loaded by a program linked -rdynamic$hand_note$shared_note" fi exit $fail diff --git a/tools/ae.c b/tools/ae.c index 4bcb71e8a..a8c8ba447 100644 --- a/tools/ae.c +++ b/tools/ae.c @@ -494,6 +494,9 @@ static const char* const k_ae_import_duties[] = { "shared_runtime", /* 1.5: the program links the shared runtime */ "runtime_abi", /* 1.11: that runtime's ABI is the library's */ "actors", /* 1.7: the program runs the scheduler */ + "own_runtime", /* #3481: the library keeps its own runtime, so what needs + * one runtime is refused (ae_refuse_split_ownership) and + * its actors' in-flight messages are warned of */ }; #define AE_CATALOG_MINOR_KNOWN 11 @@ -519,22 +522,30 @@ static int ae_elf_binds_own(const char* path) { unsigned char eh[64]; int r = -1; size_t got = fread(eh, 1, sizeof(eh), f); - if (got >= 52 && memcmp(eh, "\x7f" "ELF", 4) == 0 && (eh[4] == 1 || eh[4] == 2)) { - int is64 = eh[4] == 2, big = eh[5] == 2; + int is64 = got > 4 && eh[4] == 2; + /* The whole header of its class, and a byte order it names. */ + if (got >= (size_t)(is64 ? 64 : 52) && memcmp(eh, "\x7f" "ELF", 4) == 0 && + (eh[4] == 1 || eh[4] == 2) && (eh[5] == 1 || eh[5] == 2)) { + int big = eh[5] == 2; uint64_t phoff = is64 ? ae_elf_field(eh + 32, 8, big) : ae_elf_field(eh + 28, 4, big); int phentsize = (int)ae_elf_field(eh + (is64 ? 54 : 42), 2, big); int phnum = (int)ae_elf_field(eh + (is64 ? 56 : 44), 2, big); unsigned char ph[56]; - for (int i = 0; i < phnum && phentsize >= (is64 ? 56 : 32) && r < 0; i++) { - if (fseek(f, (long)(phoff + (uint64_t)i * (uint64_t)phentsize), SEEK_SET) != 0 || + /* PN_XNUM (0xffff) keeps the real count elsewhere: not read here. */ + for (int i = 0; i < phnum && phnum != 0xffff && phentsize >= (is64 ? 56 : 32) && r < 0; i++) { + uint64_t at = phoff + (uint64_t)i * (uint64_t)phentsize; + if (at > (uint64_t)LONG_MAX || fseek(f, (long)at, SEEK_SET) != 0 || fread(ph, 1, is64 ? 56 : 32, f) != (size_t)(is64 ? 56 : 32)) break; if (ae_elf_field(ph, 4, big) != 2) continue; /* PT_DYNAMIC */ uint64_t off = is64 ? ae_elf_field(ph + 8, 8, big) : ae_elf_field(ph + 4, 4, big); uint64_t size = is64 ? ae_elf_field(ph + 32, 8, big) : ae_elf_field(ph + 16, 4, big); int ent = is64 ? 16 : 8; - if (size > (1u << 20) || fseek(f, (long)off, SEEK_SET) != 0) break; - unsigned char* dyn = (unsigned char*)malloc(size ? size : 1); + /* An empty dynamic segment says nothing: left unknown. */ + if (size < (uint64_t)ent || size > (1u << 20) || off > (uint64_t)LONG_MAX || + fseek(f, (long)off, SEEK_SET) != 0) + break; + unsigned char* dyn = (unsigned char*)malloc(size); if (!dyn) break; if (fread(dyn, 1, size, f) == size) { r = 0; @@ -6353,8 +6364,9 @@ static int ae_emit_binimport_stub(const char* mod, const char* so_path, if (rc != 0) { remove(stub_path); return 0; } // Make the stub resolvable and link the artifact (absolute path + - // rpath so the produced binary finds it at run time). The host's - // -rdynamic + static libaether satisfy the .so's runtime symbols. + // rpath so the produced binary finds it at run time). The .so runs on + // the runtime it carries (-Bsymbolic on ELF, #3481), or on the shared + // runtime when it was built with --shared-runtime. tc_lib_dir_append_one(stubdir); char* abs_so = ae_abspath(so_path); char* dir = ae_path_printf("%s", abs_so); diff --git a/tools/ae_cross.c b/tools/ae_cross.c index 74aae5059..0deb7d4d9 100644 --- a/tools/ae_cross.c +++ b/tools/ae_cross.c @@ -1642,7 +1642,8 @@ int run_cross_build(const char* c_file, const char* out_file, * which shipped no library until selaenium's cross build.) */ /* -DAETHER_NO_LIB_MAIN: no entry point in a shared library (see the * Tier-A link below). */ - const char* fbsd_lib_flags = emit_lib ? "-shared -fPIC -DAETHER_NO_LIB_MAIN" : ""; + /* -Bsymbolic: the library keeps the runtime it carries (#3481). */ + const char* fbsd_lib_flags = emit_lib ? "-shared -fPIC -Wl,-Bsymbolic -DAETHER_NO_LIB_MAIN" : ""; /* --size strips at link time as well: same GNU/LLD spellings the Tier-A * ELF/PE path uses (FreeBSD's ld.lld takes them). */ const char* fbsd_size_link = !ae_build_size_mode() ? "" @@ -1731,7 +1732,9 @@ int run_cross_build(const char* c_file, const char* out_file, "-Wl,--out-implib,\"%s.lib\"", implib); elf_pe_lib_flags = pe_lib_flags; } else { - elf_pe_lib_flags = "-shared -fPIC"; + /* -Bsymbolic: the library binds the runtime it carries + * to itself, not to an importing program's (#3481). */ + elf_pe_lib_flags = "-shared -fPIC -Wl,-Bsymbolic"; } } /* --size strips at link time as well as suppressing debug info From 0448f55b189246d317d9a6a281a970a0ff17b861 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Fri, 30 Oct 2026 19:05:45 -0300 Subject: [PATCH 12/13] fix: own_runtime named exactly where ae refuses, and on ELF only (#3481) The review of b65fb053 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. --- compiler/aether_lib_strings.h | 136 ++++++++++++++++++ compiler/codegen/codegen.c | 85 ++++++++--- docs/abi-policy.md | 4 +- docs/emit-lib.md | 26 ++-- ...d-3481-library-keeps-its-runtime-on-elf.md | 16 ++- runtime/aether_lib_meta.h | 8 +- .../test_binary_import_actors.sh | 6 + .../test_binary_import_ownership.sh | 15 ++ tools/ae.c | 101 +++++-------- 9 files changed, 291 insertions(+), 106 deletions(-) create mode 100644 compiler/aether_lib_strings.h diff --git a/compiler/aether_lib_strings.h b/compiler/aether_lib_strings.h new file mode 100644 index 000000000..4e59452f0 --- /dev/null +++ b/compiler/aether_lib_strings.h @@ -0,0 +1,136 @@ +/* aether_lib_strings.h — which values a library's catalog spells hold a + * counted string. Two copies of the runtime cannot free each other's, so + * `ae` refuses to import such a value between a program and a library that + * keeps a runtime of its own (#2931, ae_refuse_split_ownership in + * tools/ae.c), and the compiler names the library's `own_runtime` duty for + * the same values (#3481, emit_lib_metadata in compiler/codegen/codegen.c). + * Both read the catalog's own spellings through this header, so what one + * refuses and the other names cannot drift. + * + * Standalone, like aether_lib_path.h: `tools/ae.c` reads a built library's + * catalog and has no AST. */ + +#ifndef AETHER_LIB_STRINGS_H +#define AETHER_LIB_STRINGS_H + +#include +#include +#include + +/* The type of field `k` of the struct the catalog records under the first + * `len` bytes of `name`, as the catalog spells it; NULL past its last field, + * or when the catalog records no such struct. */ +typedef const char* (*AetherLibFieldType)(const void* ctx, const char* name, size_t len, int k); + +/* A walk's state: the catalog, and the structs it has entered. A struct + * entered once is not entered again, so a struct that names itself, or + * many that name each other, cost one visit each. */ +typedef struct { + AetherLibFieldType field; + const void* ctx; + const char* seen[64]; + size_t seen_len[64]; + int seen_count; +} AetherLibStringWalk; + +static inline int aether_lib_walk_holds_string(AetherLibStringWalk* w, const char* type, int depth) { + if (!type || depth > 64) return 0; + /* A parameter's `@keep` or `@into` (#3013) is not part of its type. */ + while (*type == ' ' || *type == '@') { + if (*type == '@') + while (*type && *type != ' ') type++; + else + type++; + } + /* A typed pointer reaches its struct's strings: the program's stores + * through it release them. */ + while (*type == ' ' || *type == '*') type++; + if (strncmp(type, "struct ", 7) == 0) type += 7; + /* A tuple holds what its elements hold. */ + if (*type == '(') { + const char* el = type + 1; + int nest = 0; + for (const char* p = el; *p; p++) { + if (*p == '(' || *p == '[') nest++; + else if ((*p == ')' || *p == ']') && nest > 0) nest--; + else if (nest == 0 && (*p == ',' || *p == ')')) { + if (aether_lib_walk_holds_string(w, el, depth + 1)) return 1; + if (*p == ')') return 0; + el = p + 1; + } + } + return 0; + } + /* A function value holds none. */ + if (strncmp(type, "fn", 2) == 0 && !(isalnum((unsigned char)type[2]) || type[2] == '_')) + return 0; + /* The name its spelling starts with: `string`, `string?`, `string[]` + * and `string!` all hold one; a struct's name holds what its fields + * hold. */ + size_t n = 0; + while (isalnum((unsigned char)type[n]) || type[n] == '_') n++; + if (n == 6 && strncmp(type, "string", 6) == 0) return 1; + if (n == 0 || !w->field) return 0; + for (int i = 0; i < w->seen_count; i++) + if (w->seen_len[i] == n && strncmp(w->seen[i], type, n) == 0) return 0; + if (w->seen_count < (int)(sizeof(w->seen) / sizeof(w->seen[0]))) { + w->seen[w->seen_count] = type; + w->seen_len[w->seen_count] = n; + w->seen_count++; + } + for (int k = 0; ; k++) { + const char* ft = w->field(w->ctx, type, n, k); + if (!ft) return 0; + if (aether_lib_walk_holds_string(w, ft, depth + 1)) return 1; + } +} + +/* Whether a value of the catalog type `type` holds a counted string, + * itself or in a field, element or pointee. */ +static inline int aether_lib_type_holds_string(const char* type, AetherLibFieldType field, + const void* ctx) { + AetherLibStringWalk w; + w.field = field; + w.ctx = ctx; + w.seen_count = 0; + return aether_lib_walk_holds_string(&w, type, 0); +} + +/* The type a signature ("(a: A, b: B) -> R" or "(A, B) -> R") passes that + * holds a counted string, copied to `out`; NULL when there is none. A plain + * string crosses as a copy of itself, so it does not count here. */ +static inline const char* aether_lib_signature_string_type(const char* sig, AetherLibFieldType field, + const void* ctx, char* out, size_t cap) { + if (!sig || cap == 0) return NULL; + int depth = 0; + const char* start = sig[0] == '(' ? sig + 1 : sig; + for (const char* p = start; ; p++) { + if (*p == '(') depth++; + else if (*p == ')' && depth > 0) depth--; + else if (*p == '\0' || (depth == 0 && (*p == ',' || *p == ')'))) { + const char* t = start; + for (const char* q = start; q < p; q++) + if (*q == ':') { t = q + 1; break; } + while (t < p && (*t == ' ' || *t == '-' || *t == '>')) t++; + snprintf(out, cap, "%.*s", (int)(p - t), t); + size_t len = strlen(out); + while (len > 0 && out[len - 1] == ' ') out[--len] = '\0'; + const char* bare = out; + while (*bare == '*') bare++; + if (strncmp(bare, "struct ", 7) == 0) bare += 7; + if (strcmp(bare, "string") != 0 && aether_lib_type_holds_string(out, field, ctx)) + return out; + if (*p == '\0') return NULL; + start = p + 1; + if (*p == ')') { + /* The parameters end here; the result follows the arrow. */ + const char* arrow = strstr(start, "->"); + if (!arrow) return NULL; + p = arrow + 1; + start = arrow + 2; + } + } + } +} + +#endif /* AETHER_LIB_STRINGS_H */ diff --git a/compiler/codegen/codegen.c b/compiler/codegen/codegen.c index ddf639cbc..b87a90661 100644 --- a/compiler/codegen/codegen.c +++ b/compiler/codegen/codegen.c @@ -11,6 +11,7 @@ #include "aether_stdlib_symbols.h" /* #1366: generated */ #include "../aether_module.h" #include "../aether_error.h" +#include "../aether_lib_strings.h" /* #3481: what a library value holds, read as `ae` reads it */ #include "../analysis/actor_reply.h" #include "../analysis/sandbox_trust.h" #include "../analysis/hoist.h" @@ -4261,6 +4262,27 @@ static char* fn_source_signature_string(CodeGenerator* gen, ASTNode* fn) { return b.s; } +/* #3481: the catalog's struct records as aether_lib_strings.h reads them, + * field `k` of the recorded struct named `name` spelled as its record + * spells it, so the duty a library names is decided from what `ae` reads. */ +typedef struct { ASTNode** structs; int count; } LibCatalogStructs; + +static const char* lib_catalog_field_type(const void* ctx, const char* name, size_t len, int k) { + const LibCatalogStructs* cs = (const LibCatalogStructs*)ctx; + for (int i = 0; i < cs->count; i++) { + ASTNode* sd = cs->structs[i]; + if (!sd->value || strlen(sd->value) != len || strncmp(sd->value, name, len) != 0) continue; + for (int f = 0; f < sd->child_count; f++) { + ASTNode* fld = sd->children[f]; + if (!fld || fld->type != AST_STRUCT_FIELD || k-- > 0) continue; + const char* t = cg_type_source(fld->node_type); + return t ? t : ""; + } + return NULL; + } + return NULL; +} + static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { if (!gen || !gen->emit_lib || !program) return; @@ -4916,30 +4938,40 @@ static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { rs.result->kind == TYPE_STRING) strings_returned = 1; } int lib_runs_actors = gen->actor_count > 0 || gen->lib_actors; - /* #3481: a library with a runtime of its own keeps it on every platform - * (on ELF `ae` links it -Bsymbolic), so what needs one runtime has to - * meet an importer that knows: a struct holding strings across its edge, - * a builder that uses the config the program makes (both refused), and + /* #3481: on ELF `ae` links a library with a runtime of its own + * -Bsymbolic, so it keeps that runtime there too. What needs one runtime + * has to meet an importer that knows: what `ae` refuses to import (a + * value holding strings across the library's edge, a builder that uses + * the config the program makes; ae_refuse_split_ownership, read here + * from the same catalog spellings through aether_lib_strings.h) and * actors the program's exit cannot drain (warned of). An importer of - * 0.864.0 or before took the copies on ELF as merged and let them pass, + * 0.860.0 to 0.864.0 took the copies on ELF as merged and let them pass, * one runtime then releasing the other's strings; naming the duty makes - * it refuse the library instead. */ + * it refuse the library instead. Named for ELF only (below): elsewhere + * those importers kept the copies apart and refuse or warn themselves. */ int needs_own_runtime = 0; if (!gen->lib_shared_runtime) { if (lib_runs_actors) needs_own_runtime = 1; + LibCatalogStructs cs = { structs, struct_count }; + char oty[200]; for (int i = 0; i < fn_count && !needs_own_runtime; i++) { - LibSig os; - const char* owhy = NULL; - if (!lib_fn_signature(gen, fns[i], &os, &owhy)) continue; - for (int k = -1; k < os.count && !needs_own_runtime; k++) { - Type* t = k < 0 ? os.result : os.ptypes[k]; - while (t && t->kind == TYPE_PTR && t->element_type) t = t->element_type; - if (struct_owning_strings(gen, t)) needs_own_runtime = 1; - } - } - for (int r = 0; r < clo_count && !needs_own_runtime; r++) - if (strcmp(rec_role[r], "builder") == 0 && rec_fn[r] && !builder_never_uses_config(rec_fn[r])) + /* The signature `ae` reads: the source one when the catalog + * carries them, the display one otherwise. */ + char* disp = src_sig_needed ? NULL : fn_signature_string(gen, fns[i]); + const char* sig = disp ? disp : (src_sigs[i] ? src_sigs[i] : ""); + if (aether_lib_signature_string_type(sig, lib_catalog_field_type, &cs, oty, sizeof(oty))) needs_own_runtime = 1; + free(disp); + } + for (int r = 0; r < clo_count && !needs_own_runtime; r++) { + if (strcmp(rec_role[r], "builder") != 0) continue; + if (!builder_never_uses_config(rec_fn[r])) { needs_own_runtime = 1; break; } + char* disp = (rec_sig[r] || !rec_fn[r]) ? NULL : fn_signature_string(gen, rec_fn[r]); + const char* sig = disp ? disp : (rec_sig[r] ? rec_sig[r] : ""); + if (aether_lib_signature_string_type(sig, lib_catalog_field_type, &cs, oty, sizeof(oty))) + needs_own_runtime = 1; + free(disp); + } } /* #2957: "1.11" adds host_abi, runtime_abi and requires, the duties an * importer must perform to use this library, each one it knows by name; @@ -4947,14 +4979,14 @@ static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { * the outcome check after every call; a returned string is given back; * a shared-runtime library makes the program link the shared runtime, * whose runtime ABI must be the library's; a library that runs actors - * has the program run the scheduler. */ + * has the program run the scheduler. `own_runtime` follows the list + * where it applies (#3481, below). */ const char* schema = "1.11"; char requires[160]; - snprintf(requires, sizeof(requires), "outcome_check%s%s%s%s", + snprintf(requires, sizeof(requires), "outcome_check%s%s%s", strings_returned ? ",string_release" : "", gen->lib_shared_runtime ? ",shared_runtime,runtime_abi" : "", - lib_runs_actors ? ",actors" : "", - needs_own_runtime ? ",own_runtime" : ""); + lib_runs_actors ? ",actors" : ""); fprintf(gen->output, " \"%s\", \"", schema); fprintf(gen->output, "%s", AETHER_VERSION); /* the aetherc that wrote it */ fprintf(gen->output, "\", "); @@ -4982,8 +5014,13 @@ static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { fprintf(gen->output, "\"%s\",\n", lib_outcome_symbol(gen)); /* #2957: the host ABI level, the runtime ABI of the headers this C is * compiled against (aether_abi.h, the one the stubs check at their - * first call), and the duties. */ - fprintf(gen->output, " %d, AETHER_RUNTIME_ABI_ID, \"%s\"\n};\n\n", AETHER_HOST_ABI, requires); + * first call), and the duties. `own_runtime` only when the C is built + * for ELF, the one format whose older importers took a library's + * runtime as the program's (#3481). */ + fprintf(gen->output, " %d, AETHER_RUNTIME_ABI_ID, \"%s\"\n", AETHER_HOST_ABI, requires); + if (needs_own_runtime) + fprintf(gen->output, "#if defined(__ELF__)\n \",own_runtime\"\n#endif\n"); + fprintf(gen->output, "};\n\n"); /* #996 --emit=csrc: serialize the identical catalog as JSON alongside the * C struct. Driven by the same fns[]/closure/const tables emitted above, so @@ -5006,6 +5043,8 @@ static void emit_lib_metadata(CodeGenerator* gen, ASTNode* program) { emit_json_string(j, cg_intern_n(p, n)); p += n + (e ? 1 : 0); } + /* Listed as the C names it for an ELF target (above). */ + if (needs_own_runtime) { fputs(", ", j); emit_json_string(j, "own_runtime"); } fputs("],\n", j); if (gen->lib_shared_runtime) { fputs(" \"runtime\": ", j); emit_json_string(j, "shared"); fputs(",\n", j); diff --git a/docs/abi-policy.md b/docs/abi-policy.md index edc84e41d..3bdf45a9f 100644 --- a/docs/abi-policy.md +++ b/docs/abi-policy.md @@ -39,6 +39,7 @@ catalog asks of its importer is listed in `requires`, its duties: | `shared_runtime` | link the program against the shared runtime the library runs on (1.5) | | `runtime_abi` | check that runtime is the one the library was made for (1.11) | | `actors` | run the library's actors on the program's scheduler (1.7) | +| `own_runtime` | keep apart from the program's the runtime the library carries: refuse what needs one runtime, warn that its actors are not drained (#3481; named on ELF only, by a static-runtime library that has such an export or runs actors) | An importer that finds a duty it does not know refuses the library, naming the duty, both Aether versions and the fix (upgrade `ae`, or rebuild the @@ -91,7 +92,8 @@ catalogs and libraries compiled for a runtime of another ABI. |---|---| | A C host and a self-contained library of any version | Yes, using the functions of the library's level (read `host_abi`); a host built with older headers uses its own level's | | An Aether program importing a self-contained library | Yes, when the importer knows every duty the catalog requires; a library of an older version is read by its minor. On ELF, a library of another runtime ABI linked before #3481 (without `-Bsymbolic`, so it would run on the program's runtime) is refused at import, naming the rebuild | -| An importer older than schema 1.11 and a newer library | Calls that succeed work; it cannot read `requires`, so it does not check outcomes, and a failed call reads as its zero value, as with 0.856's own libraries | +| An importer older than schema 1.11 and a newer library | Calls that succeed work; it cannot read `requires`, so it does not check outcomes, and a failed call reads as its zero value, as with 0.856's own libraries. On ELF it also takes a library's own runtime as the program's, so it does not refuse a value that needs one runtime (#3481); rebuild such a library with `--shared-runtime` for it | +| An importer of 0.860.0 to 0.864.0 and an ELF library of a later `ae` | Refused when the library names `own_runtime` (#3481); read otherwise | | Anything on `--shared-runtime` | Only on the runtime it was made for (one runtime ABI ID) | | Two self-contained libraries of different versions in one process | Yes; each runs on its own runtime, and the shared facilities are refused across layouts | | `--emit=csrc` and `--emit=obj` | Only with the runtime of the Aether that generated them | diff --git a/docs/emit-lib.md b/docs/emit-lib.md index cf11bb4ef..007bbad74 100644 --- a/docs/emit-lib.md +++ b/docs/emit-lib.md @@ -142,7 +142,7 @@ content-addressable. Fields: | `structs[]` | Schema 1.3: the structs the exports use, `name`, `kind` (`""`, `"extern"`, `"extern_packed"`), `fields[]` (`name`, `type` as Aether source, `flags`), `source_file`, `source_line` | | `host_abi` | Schema 1.11: the host ABI level of `libaether.h` the library was built at ([abi-policy.md](abi-policy.md)) | | `runtime_abi` | Schema 1.11: the runtime ABI it was compiled for (`AETHER_RUNTIME_ABI_ID`, such as `"0.856.0/abi1"`) | -| `requires[]` | Schema 1.11: what its importer must do (`outcome_check`, `string_release`, `shared_runtime`, `runtime_abi`, `actors`); an importer that does not know one refuses the library | +| `requires[]` | Schema 1.11: what its importer must do (`outcome_check`, `string_release`, `shared_runtime`, `runtime_abi`, `actors`, and on ELF `own_runtime`); an importer that does not know one refuses the library | ```json { @@ -1291,14 +1291,22 @@ ae build app.ae -o app # imports both built static gets a warning naming the static one: rebuild it with `--shared-runtime`. - Each copy of the runtime frees only what it made. A string an export - returns crosses as a copy ([above](#strings-an-export-returns)), but a - struct holding a string (returned by value, or reached through a typed - pointer) and a builder that uses its config have none. The copies stay - apart on every platform (#3481), so importing a static library that has - such an export is an error naming each one, with the rebuild: - `--shared-runtime`. Such a library names the `own_runtime` duty in its - catalog, so an importer of 0.864.0 or before, which took ELF's copies as - merged, refuses it rather than let the copies free each other's values. + returns crosses as a copy ([above](#strings-an-export-returns)). Two + things have no copy: a value holding a string (a struct or a tuple with + one in it, passed by value or reached through a typed pointer), and the + config a builder uses. The copies stay apart on every platform (#3481), so + importing a static library that has such an export is an error naming + each one, with the rebuild: `--shared-runtime`. +- On Linux and FreeBSD such a library, and one that runs actors, names the + `own_runtime` duty in its catalog. An importer of 0.860.0 to 0.864.0 took + ELF's copies as merged; it does not know the duty, so it refuses the + whole library rather than let the copies free each other's values or + leave its actors undrained without a word. Elsewhere those importers kept + the copies apart too, so no duty is named there. An importer of 0.859.0 + or before reads no duties at all. +- A cross build (`--target`) links the target's runtime into the library, + so `--shared-runtime` with `--target` is refused: build such a library + on its target. - A sandbox's grants are each thread's, in the runtime: on one runtime a library's export called from inside another's `sandbox.enforce` block is held to the block's grants. Where the copies stay apart, the inner copy diff --git a/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md index b7b8852eb..df5e5e2e9 100644 --- a/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md +++ b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md @@ -8,13 +8,15 @@ on Linux since 0.861.0. `ae` now links such a library `-Bsymbolic`, natively and in cross builds, as macOS and Windows keep the copies apart by themselves, so on every platform a static-runtime library runs on its - own runtime: `ae` refuses an import that needs one runtime (a struct - holding strings, a builder using its config) and warns that a library's - actors are not drained at exit, naming `--shared-runtime`, as it did on - macOS and Windows. Such a library names the `own_runtime` duty in its - catalog, so an importer of 0.864.0 or before, which took ELF's copies as - merged, refuses it instead of letting the two runtimes free each other's - values. A library of another runtime ABI linked before this is refused + own runtime: `ae` refuses an import that needs one runtime (a struct or + tuple holding strings, a builder using its config) and warns that a + library's actors are not drained at exit, naming `--shared-runtime`, as + it did on macOS and Windows. On ELF such a library, and one that runs + actors, names the `own_runtime` duty in its catalog, so an importer of + 0.860.0 to 0.864.0, which took ELF's copies as merged, refuses it instead + of letting the two runtimes free each other's values. `--shared-runtime` + with `--target` is refused: a cross-built library carries the target's + runtime. A library of another runtime ABI linked before this is refused at import on ELF, naming the rebuild, instead of ending the program. A failure inside a library still reaches a `try` around the call: the import stub raises it again (#2956). This replaces 0.864.0's note that diff --git a/runtime/aether_lib_meta.h b/runtime/aether_lib_meta.h index 1537b7d88..2857a1770 100644 --- a/runtime/aether_lib_meta.h +++ b/runtime/aether_lib_meta.h @@ -105,9 +105,11 @@ * against the runtime they run on before the first call; and `requires`, * the duties its importer must perform, by name, comma-separated: * outcome_check (1.10), string_release (1.9), shared_runtime (1.5), - * runtime_abi (1.11), actors (1.7). An importer refuses a catalog that - * requires a duty it does not know, and reads one whose newer fields it - * only does not know (docs/abi-policy.md). Every catalog is "1.11". + * runtime_abi (1.11), actors (1.7), and on ELF own_runtime (#3481: the + * library keeps the runtime it carries apart from the program's). An + * importer refuses a catalog that requires a duty it does not know, and + * reads one whose newer fields it only does not know (docs/abi-policy.md). + * Every catalog is "1.11". * * Schema versioning: `schema_version` is "1.0" for function-only * artifacts, "1.1" once closure records are present, "1.2" once diff --git a/tests/integration/binary_import_actors/test_binary_import_actors.sh b/tests/integration/binary_import_actors/test_binary_import_actors.sh index ecb0666d1..ec9afdf83 100644 --- a/tests/integration/binary_import_actors/test_binary_import_actors.sh +++ b/tests/integration/binary_import_actors/test_binary_import_actors.sh @@ -57,6 +57,12 @@ run_pass() { # 1.11: every catalog has the outcome slot (#2956) and the host ABI, the runtime ABI and the duties (#2957); `actors` (1.7) is in it. echo "$INFO" | grep -q "Schema:[[:space:]]*1\.11" || { echo "$INFO"; fail "$label: lib$lib's schema is not 1.11"; } echo "$INFO" | grep -q "Actors:[[:space:]]*yes" || { echo "$INFO"; fail "$label: lib$lib's catalog does not say it runs actors"; } + # #3481: on ELF a library with a runtime of its own names `own_runtime`, + # so an importer of 0.860.0 to 0.864.0, which took that runtime as the + # program's, refuses it; elsewhere, and on the shared runtime, none does. + want=0; [ "$SO_EXT" = .so ] && [ "$drains" = 0 ] && want=1 + has=0; echo "$INFO" | grep -q "Requires:.*own_runtime" && has=1 + [ "$has" = "$want" ] || { echo "$INFO"; fail "$label: lib$lib names own_runtime: $has, expected $want"; } done OUT="$(AETHER_HOME="$ROOT" "$AE" run app.ae 2>run.log)" || { echo "$OUT"; fail "$label: ae run app.ae" run.log; } diff --git a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh index 990254191..d3253f35c 100644 --- a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh +++ b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh @@ -69,6 +69,13 @@ refused() { grep -qF "ae build --emit=lib --shared-runtime" "$1.log" || fail "$3: the refusal does not name the rebuild" "$1.log" } +# duty