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