diff --git a/Makefile b/Makefile
index 923756e44..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
@@ -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/README.md b/README.md
index 43c4987cc..dd46f1392 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; 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
+
+**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/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/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 706dfa9ce..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,13 +4938,49 @@ 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: 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.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. 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++) {
+ /* 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;
* one it does not know makes it refuse the library. The 1.10 duty is
* 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",
@@ -4956,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
@@ -4980,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/compiler/codegen/codegen_func.c b/compiler/codegen/codegen_func.c
index 0c3156d66..ea5eb6e19 100644
--- a/compiler/codegen/codegen_func.c
+++ b/compiler/codegen/codegen_func.c
@@ -1,5 +1,6 @@
#include "codegen_internal.h"
#include "../aether_error.h"
+#include "../analysis/missing_return.h"
// True when this function definition is annotated `@c_callback`.
// The annotation (#235) marks a function as having a stable, externally-
@@ -2310,6 +2311,24 @@ static void emit_function(CodeGenerator* gen, ASTNode* func, const char* clause_
fprintf(gen->output, "return ");
emit_no_match_value(gen, func);
fprintf(gen->output, ";\n");
+ } else if (gen->emit_lib && result && result->kind != TYPE_VOID && body &&
+ body->type == AST_BLOCK && body->child_count > 0) {
+ /* A body that ends in a loop nothing leaves needs no return. Under
+ * --emit=lib a deadline or a cancel leaves each loop at its head,
+ * and the call then ends here: it returns its result's zero value,
+ * as the export's wrapper does, instead of falling off the end of a
+ * function that returns one. */
+ ASTNode* last = body->children[body->child_count - 1];
+ if (last && (last->type == AST_WHILE_LOOP || last->type == AST_FOR_LOOP) &&
+ !stmt_can_complete(gen->program, last)) {
+ print_indent(gen);
+ fprintf(gen->output, "return ");
+ if (result->kind == TYPE_FUNCTION)
+ fprintf(gen->output, "(%s){0}", get_c_type(result));
+ else
+ emit_no_match_value(gen, func);
+ fprintf(gen->output, ";\n");
+ }
}
gen->current_promoted_captures = prev_promoted;
diff --git a/docs/abi-policy.md b/docs/abi-policy.md
index 540bf1d03..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
@@ -47,7 +48,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,8 +91,9 @@ 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 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 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. 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/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/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/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/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/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/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/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/emit-lib.md b/docs/emit-lib.md
index b9ea3b2d7..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
{
@@ -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:
@@ -1254,11 +1255,15 @@ 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 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
+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`,
@@ -1286,11 +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. 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`.
+ 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
@@ -1324,10 +1340,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
@@ -1459,10 +1474,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/getting-started.md b/docs/getting-started.md
index c23aeb33b..8308313dd 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:**
-
-```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):
+## A Project
```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 `ae init` writes:
```toml
[package]
@@ -587,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"
@@ -596,100 +183,69 @@ path = "src/main.ae"
[build]
target = "native"
-# link_flags = "-lsqlite3 -lcurl" # Link external C libraries
-```
-
-### Linking C Libraries
-
-To link external C libraries, add `link_flags` to the `[build]` section:
-
-```toml
-[build]
-link_flags = "-lsqlite3 -lcurl -lssl"
+# link_flags = "-lsqlite3 -lcurl" # Add extra linker flags
```
-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))
- }
-}
-```
+`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
+more directories to search.
-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
+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.
-main() {
- home = os.getenv("HOME")
- if home != null {
- println("Home directory: ${home}")
- }
-}
-```
+## Tools for When You Are Stuck
-The builtin `getenv()` also works without an import for quick scripts; its string is released with the local that holds it.
+**`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)).
-## When something doesn't compile
+**`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.
-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:
-
-```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
+
+Three documents, each with one job:
-## Next Steps
+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 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 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.
-- 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
+Start with the first chapter of the tutorial and its examples, run each
+one, and change it.
## Version Management
@@ -759,36 +315,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 +337,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 +347,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: 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**
+- `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/glossary.md b/docs/glossary.md
new file mode 100644
index 000000000..f0e99cc86
--- /dev/null
+++ b/docs/glossary.md
@@ -0,0 +1,193 @@
+# 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,
+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
+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 the process may use,
+up to 16. Each runs the actors assigned to it.
+
+**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
+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 `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.
+
+**interpolation.** `"${expr}"`: a value written into a string.
+
+**link.** A tie between two actors: when one ends, the other ends too,
+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.
+
+**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,
+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.
+
+**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 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)
+
+**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. The buffer `make([]T, n)` hands out has one
+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`.
+
+**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 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.
+
+**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/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/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/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/docs/tutorial.md b/docs/tutorial.md
index 3d7984258..129964720 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -1,507 +1,796 @@
-# 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 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:
-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
+need 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
-
-Without `wait_for_idle()`, main could exit before the actor processes its messages.
+### Data: structs, enums, sum types and match
----
+[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
-// Fibonacci
-fib(0) -> 0
-fib(1) -> 1
-fib(n) when n > 1 -> fib(n - 1) + fib(n - 2)
+[15-contracts](../examples/01-language/15-contracts.ae),
+[16-defer](../examples/01-language/16-defer.ae),
+[17-ownership](../examples/01-language/17-ownership.ae)
-// Classify a number
-classify(x) when x < 0 -> "negative"
-classify(x) when x == 0 -> "zero"
-classify(x) when x > 0 -> "positive"
+`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.
+
+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. Three rules cover it:
+
+- **Counted values**: strings, closures, string sequences, and the
+ 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`:
+
+```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 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
+ 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
+
-Actors can be spawned in any number and run concurrently across all CPU cores:
+**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.
-```aether
-message Work { id: int, value: int }
-message Done {}
+
+An answer
-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
+## 2. Actors
-### Constants
+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.
-Define named values at the top level with `const`:
+### A first actor, its state, and asking it
-```aether
-const WIDTH = 80
-const HEIGHT = 24
-const TITLE = "My App"
+[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)
-main() {
- println(TITLE)
- area = WIDTH * HEIGHT
- println("Area: ${area}")
-}
-```
+`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`:
-### Null
-
-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, 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
+
+[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 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, 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.
+
+### 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):
+**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.
-```aether
-main() {
- x = 10
- sign = if x > 0 { 1 } else { -1 }
- println(sign) // 1
-}
-```
+
+An answer
-### Range-Based For Loops
+```aether,run
+message Num { n: int }
+message Connect { next: actor_ref }
-```aether
-main() {
- // Instead of: for (i = 0; i < 10; i++) { ... }
- for i in 0..10 {
- print(i)
- print(" ")
+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 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.
+
+- **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. `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.
+
+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 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 Go library built as a C shared library in-process. [C
+interoperability](c-interop.md) and [emit=lib](emit-lib.md) are the
+references.
-### Best Practices
+---
+
+## 5. Containing code
+
+[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)
+
+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.
+
+---
-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`
+## 6. Tools
-### Further Reading
+[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)
-- [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
+`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.
---
-## 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 }` |
+## 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.
+
+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.
+
+## 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.
+- [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/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/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..714a7ba43 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,15 @@
// 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: 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 `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/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..fb0cf7040
--- /dev/null
+++ b/examples/02-actors/11-ending.ae
@@ -0,0 +1,78 @@
+// Ending an actor, and what its watchers are told.
+//
+// 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
+
+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..22edcc01f
--- /dev/null
+++ b/examples/02-actors/14-bounded-mailbox.ae
@@ -0,0 +1,79 @@
+// 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) -> {
+ for i in 1..n + 1 {
+ err = try_send(self, Line { n: i })
+ if err == "" {
+ println("line ${i}: sent")
+ } else {
+ println("line ${i}: ${err}")
+ }
+ }
+ }
+ Flood(n) -> {
+ for i in 1..n + 1 {
+ self ! Line { n: i } // past the bound: dropped and counted
+ }
+ }
+ 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..92556726b
--- /dev/null
+++ b/examples/02-actors/15-streams.ae
@@ -0,0 +1,86 @@
+// 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() -> {
+ while sent < total {
+ if stream.offer(s, "log line ${sent + 1}") != 1 {
+ break // 0: no credit left, wait for the next one
+ }
+ sent = sent + 1
+ }
+ 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/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
new file mode 100644
index 000000000..fd8058091
--- /dev/null
+++ b/examples/03-std/10-tcp.ae
@@ -0,0 +1,85 @@
+// TCP: a server and a client over a socket, in one program.
+//
+// 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
+
+import std.string
+import std.tcp
+
+// A socket, and what has arrived on it past the last line read.
+struct LineReader {
+ sock: ptr
+ pending: string
+}
+
+// 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
+ }
+ r.pending = string.concat(r.pending, part)
+ }
+}
+
+// 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)
+ check("listen", err)
+ defer 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)
+ // 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.
+ 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}'")
+ 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.
+ 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:
+// 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..12a729f52
--- /dev/null
+++ b/examples/03-std/11-cryptography.ae
@@ -0,0 +1,66 @@
+// 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. 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
+
+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(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
+ // (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 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
new file mode 100644
index 000000000..43e1fcb5c
--- /dev/null
+++ b/examples/04-interop/07-limits/host.c
@@ -0,0 +1,76 @@
+/* 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 = { ms / 1000, (long)(ms % 1000) * 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 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);
+
+ /* 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..25896dd61
--- /dev/null
+++ b/examples/04-interop/07-limits/script.ae
@@ -0,0 +1,50 @@
+// 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)`, 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;
+// - 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 = ""
+ for i in 0..n {
+ s = string.concat(s, "0123456789abcdef")
+ }
+ 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/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/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..54385949a
--- /dev/null
+++ b/examples/08-rosetta/06-sieve.ae
@@ -0,0 +1,35 @@
+// 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, 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
+// See: https://rosettacode.org/wiki/Sieve_of_Eratosthenes
+
+primes_up_to(limit: int) -> int[] {
+ crossed = make([]bool, limit + 1)
+ 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..37aa71c22
--- /dev/null
+++ b/examples/08-rosetta/08-n-queens.ae
@@ -0,0 +1,84 @@
+// 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)
+ first = make([]int, n)
+ 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..269d281d0
--- /dev/null
+++ b/examples/08-rosetta/19-levenshtein.ae
@@ -0,0 +1,46 @@
+// 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 its local owns and releases: 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)
+ 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..e8419cbd0
--- /dev/null
+++ b/examples/08-rosetta/23-josephus.ae
@@ -0,0 +1,43 @@
+// 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)
+ 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 77e026dff..cd4d10266 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 (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.
## 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
@@ -127,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`, and its string
+reversal is `input()[::-1]`), 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) | 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) | 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 |
+| [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) | 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) | 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 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]`).
+
## Writing an example
An example is one `.ae` file, or a directory when it needs more than one
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/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..a0ad0035d
--- /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).** 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
+ 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`.
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..2270e2e1d
--- /dev/null
+++ b/new_changelogs/20261030T130100Z-changed-3475-one-learning-path.md
@@ -0,0 +1,15 @@
+- **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`.
+ 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/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.
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.
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..df5e5e2e9
--- /dev/null
+++ b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md
@@ -0,0 +1,23 @@
+- **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`,
+ 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 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
+ the matrix expects ELF's merged runtimes.
diff --git a/new_changelogs/20261030T200000Z-fixed-lib-loop-nothing-leaves-returns-zero.md b/new_changelogs/20261030T200000Z-fixed-lib-loop-nothing-leaves-returns-zero.md
new file mode 100644
index 000000000..e82d0fb8f
--- /dev/null
+++ b/new_changelogs/20261030T200000Z-fixed-lib-loop-nothing-leaves-returns-zero.md
@@ -0,0 +1,8 @@
+- **A library function that ends in a loop nothing leaves returns its zero
+ value when a deadline or a cancel ends it.** Such a function needs no
+ `return`, but under `--emit=lib` the safe point at each loop head leaves
+ the loop, and the C then fell off the end of a function that returns a
+ value: clang warned (`-Wreturn-type`, into the output of
+ `examples/04-interop/07-limits`), and the value the export's wrapper read
+ was undefined. The function now returns its result's zero value there,
+ as the wrapper does.
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/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/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/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 67f15d2c6..576f9be63 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
@@ -159,40 +162,57 @@ 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
}
-# On ELF a library with a runtime of its own binds that runtime's calls to
-# the importer's, which every Aether program exports (-rdynamic): the copies
-# merge, as binary_import_actors relies on. A library made for another
-# runtime ABI then runs on the importer's runtime, so its check must refuse
-# it and say why. Mach-O and PE keep the copies apart, and a level-0 library
-# records no ID and has no check.
+# On ELF this ae links a library with a runtime of its own -Bsymbolic, so it
+# keeps that runtime in any program (#3481), as Mach-O and PE keep the copies
+# apart by themselves. A library linked before that binds its runtime's calls
+# to the importer's, which every Aether program exports (-rdynamic), and so
+# would run on the importer's runtime: one made for another runtime ABI is
+# refused by this importer at build, naming the rebuild, instead of the
+# program ending on its first call. A level-0 library records no ID and has
+# no check.
static_abi() { "$TMP/host_new$exe" "$1" "$1" --runtime-abi 2>/dev/null; }
-run_app_refused() {
+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 did not build" "$2/app_build.log"
- return
- fi
- # The refusal is a panic: an inner shell keeps its "Aborted" off the log.
- got="$(cd "$2" && LD_LIBRARY_PATH="$2${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
- sh -c './app 2>app.err; exit $?' 2>/dev/null)"
- if [ -n "$got" ] || ! grep -q "the library was made for Aether runtime" "$2/app.err"; then
- bad "$1 (another runtime ABI, merged on ELF): not refused with its reason" "$2/app.err"
+ 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
}
+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
if [ "$ext" = so ] && [ -n "$old_static_abi" ] &&
[ "$old_static_abi" != "$(static_abi "$TMP/new/libmlib.$ext")" ]; then
- run_app_refused "this importer, the $old_version library" "$TMP/old" "$ROOT" "$AE"
- run_app_refused "the $old_version importer, this library" "$TMP/new" "$OLD_HOME" "$OLD_AE"
- app_note=" (refused across runtime ABIs, the ELF copies merging)"
-else
- 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"
- app_note=""
+ 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
+app_note=""
+case "$old_binds_own" in
+ 0) refuse_app "this importer, the $old_version library (linked before #3481)" "$TMP/old" "$ROOT" "$AE"
+ app_note=" (the $old_version library, linked before #3481, refused at import)" ;;
+ 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,
# $4 = optional host mode:
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..ec9afdf83 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() {
@@ -59,13 +57,19 @@ 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; }
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 +84,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..d3253f35c 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
@@ -70,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