@elaraai/east-claude-plugin 1.0.99 → 1.0.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build/mcp/server.js +3 -2
- package/.claude-plugin/plugin.json +1 -1
- package/index.json +12207 -216
- package/package.json +1 -1
- package/skills/e3/SKILL.md +29 -802
- package/skills/e3/reference/browser.md +93 -0
- package/skills/e3/reference/cli.md +120 -0
- package/skills/e3/reference/from-code.md +48 -0
- package/skills/e3/reference/rack.md +139 -0
- package/skills/e3/reference/record-indexes.md +326 -0
- package/skills/e3/reference/record-migrations.md +63 -0
- package/skills/e3/reference/repository.md +25 -0
- package/skills/east/SKILL.md +30 -459
- package/skills/east/reference/beast2.md +71 -0
- package/skills/east/reference/cross-language-and-json-schema.md +87 -0
- package/skills/east/reference/queries.md +46 -0
- package/skills/east/reference/runners-and-ir.md +148 -0
- package/skills/east/reference/trees-and-recursion.md +56 -0
- package/skills/east/reference/values-in-typescript.md +62 -0
- package/skills/east-py/SKILL.md +28 -1647
- package/skills/east-py/reference/beast2.md +108 -0
- package/skills/east-py/reference/cli.md +51 -0
- package/skills/east-py/reference/columns-and-platform-functions.md +87 -0
- package/skills/east-py/reference/control-flow-and-queries.md +147 -0
- package/skills/east-py/reference/cross-language.md +70 -0
- package/skills/east-py/reference/ir-and-diagnostics.md +150 -0
- package/skills/east-py/reference/patterns.md +369 -0
- package/skills/east-py/reference/performance.md +70 -0
- package/skills/east-py/reference/value-methods.md +306 -0
- package/skills/east-py/reference/values-construction-and-json.md +88 -0
- package/skills/east-py/reference/writing-east-functions.md +275 -0
- package/skills/east-py-datascience/SKILL.md +11 -28
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Binary serialization (beast2)
|
|
2
|
+
|
|
3
|
+
beast2, the binary form every runtime and e3 store, and collections larger than memory. Part of the `east` skill.
|
|
4
|
+
|
|
5
|
+
## Binary serialization (beast2)
|
|
6
|
+
|
|
7
|
+
Inside a body the builtins are `East.Blob.encodeBeast(value, 'v2')` and
|
|
8
|
+
`blob.decodeBeast(type, 'v2')`; the functions below are the **host-side**
|
|
9
|
+
TypeScript API for the same format. Blobs are self-describing, and a decoder
|
|
10
|
+
reads any container version its type was written in.
|
|
11
|
+
|
|
12
|
+
| Signature | Description |
|
|
13
|
+
|-----------|-------------|
|
|
14
|
+
| **Whole values** |
|
|
15
|
+
| `encodeBeast2For(T, opts?): (value) => Uint8Array` | `opts.version` (`5` default, `4` for legacy readers); `opts.codec` (`"deflate"` \| `"none"`) and `opts.index` for v5 |
|
|
16
|
+
| `decodeBeast2For(T, opts?): (blob) => ValueTypeOf<T>` | Any container version, in every runtime and browser. The blob's header type must be `T`, or a subtype whose variant cases line up with `T`'s (a `none` reads as any `Option`); any other is refused — `beast2: cannot decode a blob of type … as …`, the words east-c and east-py use. `opts.platform` for decoded functions |
|
|
17
|
+
| `decodeBeast2(blob)` → `{ type, value }` | Without knowing the type |
|
|
18
|
+
| `decodeBeast2ForAsync(T)` | The same, decompressing through the platform's `DecompressionStream` — faster for large blobs in a browser |
|
|
19
|
+
| `encodeEastIR(ir)` · `decodeEastIR(blob)` | A program with its source map |
|
|
20
|
+
| **Collections larger than memory** (v5) |
|
|
21
|
+
| `new Beast2ElementWriter(T, sink, opts?)` | The canonical writer: `.add(element)` one Array/Set element or Dict `[key, value]` — Set elements and Dict keys strictly ascending in East order — and the content-defined cut rule places the segments, so the bytes are a function of the value, identical from east-c and east-py, and a one-row edit re-cuts only the segments around it. Memory is one segment; `.finish()` writes the last, the terminator and the index. `sink` is `(bytes) => void`, or `{ segment(s) }` for one standalone segment blob at a time (`s`: `{ count, fence, logicalBytes, blob }`, under `writer.header`) |
|
|
22
|
+
| `encodeBeast2PagedFor(T)(value)` | One value in memory through the element writer; plain `Map`/`Set` inputs are sorted first |
|
|
23
|
+
| `new Beast2RunSorter(T, openRun, { merge } \| { union })` | Elements in ANY order in, sorted canonical runs out (a key added twice folds with `merge(key, acc, value)`, or `union` for a Set; without one it throws); then `mergeBeast2For(T, { merge } \| { union })(runs, sink)` writes the value. Memory is one run, then one segment per run. A run closes at `RUN_MAX_COUNT` elements or `RUN_MAX_BYTES`, platform constants, so every runtime writes the same runs |
|
|
24
|
+
| `mergeBeast2For(T, opts?)(sources, sink)` | Sorted collections — blobs, range readers or manifests — into their union's blob or manifest, segment by segment; a key several hold folds in input order; `opts.from` / `opts.to` merge a key range |
|
|
25
|
+
| `recutBeast2For(T)(pieces, sink)` | A collection in pieces, some already written, into its canonical segments: a segment the whole shares with its piece is carried unread (`sink.carried`), the rest re-cut (`sink.written`) |
|
|
26
|
+
| `intakeBeast2For(T)(delivery, sink, opts?)` | A collection from outside — a `Beast2SyncRangeReader` over a v4 or v5 blob whose header names exactly `T` — through the Writer into a `Beast2ManifestSink`, a segment at a time: each row is walked by its type, the Writer's own bytes go on as they stand, and any other row is decoded and written again. `opts.segments` takes in the index's segments `[from, to)`, a piece of a large delivery. Refuses another type, a malformed segment or one over `RUN_MAX_BYTES`, a row that does not decode, and keys that do not ascend, with `Beast2IntakeError` in the words every runner uses. What e3's intake units run, on every runner |
|
|
27
|
+
| `new Beast2Writer(T, sink)` · `encodeBeast2SegmentsFor(T)(batches)` | Segments of your own choosing: one per non-empty batch, Set/Dict batches in ascending order |
|
|
28
|
+
| `iterBeast2SegmentsFor(T)(blob)` | One decoded segment at a time |
|
|
29
|
+
| `openBeast2PagesFor(T)(source)` | Random access: `.elementCount` and `.segmentCount` in O(1); `.segment(i)`, `.element(row)`, `.slice(offset, limit)` and `.get(key)` decode only the segments they touch |
|
|
30
|
+
| `openBeast2LazyFor(T, { frozen? })(source)` | An ordinary `SortedMap` / `SortedSet` / array served from the index, a segment at a time — what `blob.openBeast` and `FileSystem.openBeast` return, frozen. `isBeast2LazySafe(T, { frozen })`: only `Ref`- and function-bearing shapes decode whole |
|
|
31
|
+
| `Beast2SyncRangeReader` — `{ size, read(offset, length) }` | Positioned reads, wherever a blob goes, so a file's bytes never sit on the heap whole; `readBeast2Extents` / `readBeast2ExtentsRanged` (HTTP, S3) read the geometry |
|
|
32
|
+
| **Segment manifests** — one object per segment plus a manifest naming them: how e3 stores a collection |
|
|
33
|
+
| `new Beast2ManifestWriter(T, { object(hash, bytes), manifest(bytes) })` | The canonical manifest directory: the header, each segment as it is cut (named by its SHA-256, `<manifest>.segments/<hash>.beast2` on disk), then the manifest; east-c and east-py write the same |
|
|
34
|
+
| `readBeast2Manifest(source)` | The manifest a blob holds (`{ kind, level, type, rule, header, entries: [{ hash, fence, count, bytes }] }`), or `null`; `manifestElementCount` / `manifestByteSize` total it |
|
|
35
|
+
| `{ manifest, segment(i) }` (`Beast2ManifestSource`) | A manifest-held collection wherever a blob goes; `segment(i)` is called only for a segment a read decodes |
|
|
36
|
+
| `spliceBeast2Segments(head, segments)` | A manifest's segments spliced back into one blob, a chunk at a time, decoding nothing and holding only the segment in hand — how e3-api-client downloads a collection. Throws on a segment that is not a segmented, indexed v5 collection, or was written under another header |
|
|
37
|
+
| `sha256Hex(bytes)` | SHA-256 in lowercase hex, pure TypeScript |
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { ArrayType, StructType, StringType, IntegerType,
|
|
41
|
+
Beast2ElementWriter, iterBeast2SegmentsFor, openBeast2PagesFor, decodeBeast2For } from "@elaraai/east";
|
|
42
|
+
|
|
43
|
+
const Rows = ArrayType(StructType({ id: IntegerType, name: StringType }));
|
|
44
|
+
|
|
45
|
+
const out = createWriteStream("rows.beast2");
|
|
46
|
+
const writer = new Beast2ElementWriter(Rows, bytes => out.write(bytes));
|
|
47
|
+
for (const row of rows) writer.add(row); // never whole in memory
|
|
48
|
+
writer.finish();
|
|
49
|
+
|
|
50
|
+
const blob = new Uint8Array(readFileSync("rows.beast2"));
|
|
51
|
+
decodeBeast2For(Rows)(blob); // whole: segments concatenate
|
|
52
|
+
for (const segment of iterBeast2SegmentsFor(Rows)(blob)) consume(segment); // a segment at a time
|
|
53
|
+
openBeast2PagesFor(Rows)(blob).element(4_000_000); // one row: decodes ONE segment
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- Set and Dict segments hold the canonical value — strictly ascending,
|
|
57
|
+
disjoint, no duplicate keys — so writers refuse out-of-order elements and
|
|
58
|
+
decoders refuse a non-canonical blob as corrupt.
|
|
59
|
+
- **One encoding per value**: the element writer and `encodeBeast2PagedFor` cut
|
|
60
|
+
by the same rule in every runtime, so equal values are equal bytes and share
|
|
61
|
+
segments in a content-addressed store. `Beast2Writer` writes the batches you
|
|
62
|
+
give it, which every reader also reads.
|
|
63
|
+
- Streaming and paging are v5 only. The writer's segments are self-contained,
|
|
64
|
+
with an index, which paging and parallel decode need: pass
|
|
65
|
+
`{ selfContained: false }` only when aliasing must span segments. `write()`
|
|
66
|
+
skips an empty batch, so a segment count is never zero.
|
|
67
|
+
- Inside a body, `blob.openBeast(T)` and `FileSystem.openBeast(T, path)` hand
|
|
68
|
+
back the frozen lazy value a runner gives a large task input: keyed reads and
|
|
69
|
+
`$.for` cost one segment, mutation is refused (`.copy()` first), and the
|
|
70
|
+
header type is checked against `T` first. `East.Blob.encodeBeast` writes no
|
|
71
|
+
index, so a paged blob comes from the writers above or a runner's output.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Cross-language functions and JSON contracts
|
|
2
|
+
|
|
3
|
+
`East.importFunction` / `East.exportFunctions`, and `jsonSchemaFor` / `typeFromJsonSchema`. Part of the `east` skill.
|
|
4
|
+
|
|
5
|
+
## Cross-language functions: `East.importFunction` / `East.exportFunctions`
|
|
6
|
+
|
|
7
|
+
A function authored in python (with its `East.function`) is called from a
|
|
8
|
+
TypeScript body by name, and the other way round; the deployed program is pure
|
|
9
|
+
IR. Functions travel as a **manifest** — their IR, declared types and platform
|
|
10
|
+
dependencies. In an e3 project `e3.export` builds and links manifests itself
|
|
11
|
+
(see **e3**); by hand:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
east-py export-functions pricing -o pricing.functions.beast2 -p east-py-std # python side
|
|
15
|
+
east-node export-functions dist/maths.js -o maths.functions.beast2 # reads the module's `eastFunctions`
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
const score = East.importFunction("pricing", "score", FunctionType([RowType], FloatType)); // typed, callable
|
|
20
|
+
const total = East.function([ArrayType(RowType)], FloatType, ($, rows) => rows.map(($, r) => score(r)).sum());
|
|
21
|
+
|
|
22
|
+
// in-process: link, then compile — linkImports returns a sync or async function's IR
|
|
23
|
+
const { ir } = East.linkImports(total, [East.decodeFunctionManifest(readFileSync("pricing.functions.beast2"))]);
|
|
24
|
+
new EastIR(ir as FunctionIR).compile([])(rows);
|
|
25
|
+
|
|
26
|
+
// export TypeScript functions for python to import
|
|
27
|
+
writeFileSync("maths.functions.beast2", East.encodeFunctionManifest(East.exportFunctions("maths", "1.0.0", { double, halve })));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- The declared type must equal the exported type **exactly** — a mismatch is a
|
|
31
|
+
build error naming both. Unlinked, the reference is a `Platform` node named
|
|
32
|
+
`east.importFunction`, so compiling it without linking fails loudly.
|
|
33
|
+
- Only closed values export: no captures, and no unresolved imports of their own.
|
|
34
|
+
- A manifest carries each function's source map: `linkImports` adds its
|
|
35
|
+
locations to the importer's map, which it returns as `sourceMap`, so an
|
|
36
|
+
error or a profile names the exporter's source — the python line included.
|
|
37
|
+
- The imported function's platform calls must be provided by the consuming
|
|
38
|
+
runner (stock families count across runtimes: `east-py-std` ≡
|
|
39
|
+
`@elaraai/east-node-std` ≡ `east-c-std`). See `docs/conventions/EAST_CODEGEN.md`.
|
|
40
|
+
|
|
41
|
+
## A JSON contract for a type: `jsonSchemaFor` / `typeFromJsonSchema`
|
|
42
|
+
|
|
43
|
+
`East.String.printJson` writes East JSON, lossless and self-describing.
|
|
44
|
+
`jsonSchemaFor(T)` emits the JSON Schema of exactly that encoding, so a third
|
|
45
|
+
party can validate a payload before sending it; `typeFromJsonSchema` goes the
|
|
46
|
+
other way. Both are host-side, like `compareFor`.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
writeFileSync("contract.schema.json", JSON.stringify(jsonSchemaFor(ArrayType(ReadingType), { draft: "draft-07" }), null, 2));
|
|
50
|
+
const T = typeFromJsonSchema(JSON.parse(readFileSync("partner.schema.json", "utf-8")));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Signature | Description |
|
|
54
|
+
|-----------|-------------|
|
|
55
|
+
| `jsonSchemaFor(type, options?: { draft }): JsonSchema` | The schema of `type`'s East JSON. `draft`: `"2020-12"` (default), `"draft-07"` or `"openapi-3.0"`. Throws on `Never`, `Function`, `AsyncFunction` |
|
|
56
|
+
| `typeFromJsonSchema(schema): EastType` | The type a schema describes. Throws `JsonSchemaUnsupportedError`, with the RFC 6901 pointer, on a keyword East cannot express (`allOf`, `not`, `if`/`then`/`else`, `anyOf`, an open record, …) |
|
|
57
|
+
| `EAST_JSON_PATTERNS` | The lexical forms of East JSON's scalars (`integer`, `blob`, `floatSpecials`), for a reader enforcing exactly what the schema says |
|
|
58
|
+
|
|
59
|
+
- **An `Option<T>` is `null` or `T`'s own encoding** wherever `T` can never
|
|
60
|
+
encode as `null` (`none` → `null`, `some(7n)` → `"7"`; a struct field is
|
|
61
|
+
`"note": null` or `"note": "x"`, and an absent key is still an error). Only
|
|
62
|
+
`Option<Null>` and `Option<Option<T>>` keep the tagged
|
|
63
|
+
`{"type": …, "value": …}` object. A `Recursive` payload is judged by what it
|
|
64
|
+
wraps. The rule is a total function of the type, the same on every runtime,
|
|
65
|
+
and the tagged object under a flat Option is refused by the payload's decoder
|
|
66
|
+
(`expected string, got {"type":"none","value":null}`).
|
|
67
|
+
- **A `DateTime` is `format: "date-time"`**: every decoder reads any RFC 3339
|
|
68
|
+
date-time — `Z` or any offset (read as UTC), `t`/`z` in either case, any
|
|
69
|
+
fractional digits (past the millisecond dropped, never rounded), a leap second
|
|
70
|
+
as the Unix time its fields add up to — in years 0001–9999. `printJson`
|
|
71
|
+
writes one form: UTC, three fractional digits, `+00:00`.
|
|
72
|
+
- **Other scalars are described as the encoder emits them**: an `Integer` a
|
|
73
|
+
quoted i64 decimal, a `Blob` lowercase hex (`parseJson` also takes uppercase,
|
|
74
|
+
which the contract does not), so a producer that validates never sends what
|
|
75
|
+
the strict reader rejects. The patterns spell digits `[0-9]`, so python and
|
|
76
|
+
JavaScript validators accept the same strings.
|
|
77
|
+
- **The document is deterministic and identical across languages** (east-py's
|
|
78
|
+
`json_schema_for` emits the same bytes), and its `x-east-type` annotations
|
|
79
|
+
make the inverse exact: `Set` vs `Array`, `Dict` vs an array of pairs, a flat
|
|
80
|
+
Option vs any `oneOf`. A foreign schema without them converts by a
|
|
81
|
+
documented structural mapping; `nullable: true`, `["string", "null"]` and a
|
|
82
|
+
`oneOf` of null and one schema read as an Option, and a
|
|
83
|
+
`format: "date-time"` string as a `DateTime`.
|
|
84
|
+
- **Recursion binds one `RecursiveType` per cycle group**; definitions that
|
|
85
|
+
need two binders are refused, naming them.
|
|
86
|
+
- To READ a JSON document larger than memory under a contract, use `Json.open`
|
|
87
|
+
/ `Json.next` from **east-node-std** (and its east-py-std and east-c-std twins).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Queries (typed jq)
|
|
2
|
+
|
|
3
|
+
`East.jq` inside an East body, and the host-side checker. Part of the `east` skill.
|
|
4
|
+
|
|
5
|
+
## Queries (typed jq)
|
|
6
|
+
|
|
7
|
+
`East.jq` asks a question of a value in jq, inside an East body. The program is
|
|
8
|
+
parsed, type-checked against the input's East type and translated to ordinary
|
|
9
|
+
IR when the function is built, so it runs on every runner; a mistake is a
|
|
10
|
+
`QueryError` at build time, with the checker's sentence, span and fixes. The
|
|
11
|
+
language is jq 1.8 over East values: `libs/east/devdocs/QUERY.md` in the East
|
|
12
|
+
repository says what each builtin does, and every place East differs from jq.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
const Order = StructType({ id: IntegerType, total: FloatType });
|
|
16
|
+
|
|
17
|
+
const bigOrders = East.function([ArrayType(Order)], ArrayType(IntegerType), ($, orders) =>
|
|
18
|
+
East.jq(orders, "[.[] | select(.total > 1000) | .id]", ArrayType(IntegerType)));
|
|
19
|
+
|
|
20
|
+
// An object of inputs is read as an e3 root: `.orders` is that input alone, so a lazy one stays lazy.
|
|
21
|
+
const revenue = East.function([ArrayType(Order)], FloatType, ($, orders) =>
|
|
22
|
+
East.jq({ orders }, ".orders | map(.total) | add", FloatType));
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Signature | Description | Example |
|
|
26
|
+
|-----------|-------------|---------|
|
|
27
|
+
| **In an East body** |
|
|
28
|
+
| `East.jq<T>(input: Expr \| { [name: string]: Expr }, program: string, resultType: T): ExprType<T>` **❗** | The query's result, an expression of `resultType`: the outputs' type `T` for a query that gives exactly one output, `Option<T>` for at most one, `Array<T>` for any number. Throws `QueryError` when the query does not check, or checks to another type | `East.jq(orders, "map(.total)", ArrayType(FloatType)).sum()` |
|
|
29
|
+
| **Host-side** |
|
|
30
|
+
| `checkJq(program: string, input: EastType, options?: { root?: boolean, tooling?: boolean }): CheckJqResult` | The checker: `program` (the program as written, a `JqType` tree, or `null` when it does not check), `inputType`, `diagnostics` (each a code, a span, one sentence, suggestions and fixes), `elementType`, `multiplicity` (`"one"`, `"maybe"` or `"many"`). A query's types are never stored: check the program against its data wherever they are needed | `checkJq(".orders[0].totl", Root).diagnostics[0].message` |
|
|
31
|
+
| `completeJq(text: string, offset: number, input: EastType, options?): JqCompletions \| null` | The completions at a cursor, from the type there: fields, cases, variables, builtins | `completeJq(".orders[0].to", 13, Root)` |
|
|
32
|
+
| `printJq(program: JqNode, options?: { layout?: "line" \| "pipeline" }): PrintedJq` | A program's canonical text, and its nodes' spans; `pipeline` puts each top-level stage on a line of its own | `printJq(checked.program!).text` |
|
|
33
|
+
| `translateJq(checked: CheckJqResult, options?: { maxOutputs?: number, tooling?: boolean }): JqTranslation` **❗** | The translation: `fn()`, an East function of the inputs the query reads (`inputs`), and its `resultType` | `East.compile(translateJq(checked).fn(), [])` |
|
|
34
|
+
| `evaluateJq(program: string \| CheckJqResult, input: unknown, options?: { inputType?, root?, tooling?, platform? }): unknown` **❗** | Checks, translates, compiles (cached) and runs a query over a TypeScript value. Throws `QueryError`: the checker's diagnostics, or one `runtime` diagnostic at the node that raised | `evaluateJq("[.[] \| .id]", orders, { inputType: ArrayType(Order) })` |
|
|
35
|
+
| `splitJq(checked: CheckJqResult, options?: { maxOutputs?: number }): JqSplit` | A query checked as an e3 root, split over one dataset's pieces for an e3 split call: `piece()` each piece runs, the `output` kind its outputs combine by (`array`, `set`, `dict` with `merge()`, `fold` with `zero` and `combine()`), and `then()` over the result; or, `kind: "whole"`, why it runs as one unit. QUERY.md §17.1 | `splitJq(checkJq(".orders \| group_by(.region) \| map(length)", Root, { root: true })).output.kind` |
|
|
36
|
+
|
|
37
|
+
- **The values are East's.** A Dict is an object keyed by its key type, so
|
|
38
|
+
`.byId[1035]` looks up an Integer key; a variant reads as `{type, value}`,
|
|
39
|
+
and `select(.status.type == "shipped")` narrows it; an Option is `null` or
|
|
40
|
+
its value. `.name` on a struct that lacks the field is an error (`.name?`
|
|
41
|
+
gives `null`), and so is comparing values of two types.
|
|
42
|
+
- **A lookup can miss**, so `.[i]` and `.[k]` give an `Option`, and
|
|
43
|
+
`first(f)` gives at most one output, so its result type is `Option<T>`.
|
|
44
|
+
- **In code a query is a call of the `Query` builtin**, which carries the
|
|
45
|
+
program as written beside its translation: every runtime runs it as any
|
|
46
|
+
builtin, and `East.toSource` prints it back as `East.jq(…)`.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Runners, units, IR and checking a module
|
|
2
|
+
|
|
3
|
+
Running a program from the command line, a runner-protocol unit in a host of your own, IR to source, and `east-node check`. Part of the `east` skill.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- Runners: running a program from the command line
|
|
7
|
+
- Units in a host of your own: `executeUnit` and `UnitIO`
|
|
8
|
+
- IR → source: `East.toSource` and `east-node transpile`
|
|
9
|
+
- Checking that a module builds: `east-node check`
|
|
10
|
+
|
|
11
|
+
## Runners: running a program from the command line
|
|
12
|
+
|
|
13
|
+
A program's IR runs on any of three runners, which take the same arguments.
|
|
14
|
+
Write it with `writeFileSync("prog.beast2", encodeEastIR(fn.toIR()))` — which
|
|
15
|
+
keeps its source map, so errors name your lines — or `fn.toIR().toJSON()`.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
east-node run prog.beast2 -p @elaraai/east-node-std -i a.beast2 -i b.beast2 -o out.beast2 # -v: timing and peak memory
|
|
19
|
+
east-c run prog.beast2 -p east-c-std -i a.beast2 -o out.beast2 [--profile] # native; --profile: time per function
|
|
20
|
+
east-py run prog.beast2 -p east-py-std -i a.beast2 -o out.beast2 [--profile] # python (the east-py skill)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `--profile` (east-c and east-py, `run` and `exec`) prints every East
|
|
24
|
+
function and platform function the work called, by self time, with its
|
|
25
|
+
calls and where it is defined and called. `EAST_PROFILE=1` profiles without
|
|
26
|
+
the flag — e3 hands its environment to the runners, so a task's report
|
|
27
|
+
lands in its log — and `EAST_PROFILE_INTERVAL=N` prints the profile so far
|
|
28
|
+
every N seconds while the work runs.
|
|
29
|
+
- Each `-i` is one argument, in parameter order, and each `-p` loads one
|
|
30
|
+
platform package (east-node needs at least one); `-o` writes the result,
|
|
31
|
+
else it is printed. IR and values may be `.beast2`, `.beast`, `.east` or
|
|
32
|
+
`.json`.
|
|
33
|
+
- `exec <unit>` is the runner protocol e3 speaks: it runs one unit file — a
|
|
34
|
+
program over its inputs, a merge of an output's parts, or the intake of a
|
|
35
|
+
delivered collection — writes the output by its kind and records a result;
|
|
36
|
+
exit 0 when it succeeded, 1 when it failed.
|
|
37
|
+
- `version [-p <package>…]` prints the runner's version and each package's.
|
|
38
|
+
- east-c also has `convert <file> [-o <file>] [--type <east-type>]`, a value
|
|
39
|
+
from one format to another, and the IR toolbox: `ir normalize` (the canonical
|
|
40
|
+
form — the round-trip equality contract), `ir diff a b` (the first structural
|
|
41
|
+
difference) and `ir convert` (JSON ↔ beast2, the source map kept).
|
|
42
|
+
- east-node also has `transpile`, `check`, `lsp` and `export-functions`, below;
|
|
43
|
+
east-py has the python twins, and `lint`.
|
|
44
|
+
|
|
45
|
+
### Units in a host of your own: `executeUnit` and `UnitIO`
|
|
46
|
+
|
|
47
|
+
east-node's `exec` is a file wrapper around `executeUnit`, the one TypeScript
|
|
48
|
+
unit runner, which does a unit's work over a `UnitIO` rather than a file
|
|
49
|
+
system — so a host with none runs units too: e3-web runs them in a browser's
|
|
50
|
+
Web Workers. east-c has the same shape (`east/unit.h`).
|
|
51
|
+
|
|
52
|
+
| Signature | Description |
|
|
53
|
+
|-----------|-------------|
|
|
54
|
+
| `executeUnit(unit: Unit, io: UnitIO, options: ExecuteUnitOptions): Promise<UnitResult>` | Does the unit's work — a run, a merge or an intake — over `io`, writing its output there by its kind, and returns the result, which the host writes where `unit.result` says if it keeps it. A failure is the result's `outcome`, `failed` with its message and source locations, never a throw |
|
|
55
|
+
| `options.platforms(name: string): readonly PlatformFunction[] \| Promise<…>` | Each package the unit lists, asked once, in the unit's order, before the work begins; a throw fails the unit with its message |
|
|
56
|
+
| `options.resident?: () => number` · `options.peakBytes?: () => bigint` | The host's memory gauges. Without `resident`, what reading a lazy input whole weighs and what decoding an input whole added both come to 0; without `peakBytes`, the result's `peakBytes` is 0, as a browser's is |
|
|
57
|
+
| `options.report?: UnitRunReport` | Hears how a run unit's inputs open and what reading them came to: the account `exec -v` prints |
|
|
58
|
+
| `UnitIO` | The unit's files, by the paths it names: `read`, `size`, `readRange`, `segment(path, fetch)`, `list`, `makeDirectory`, `write` — every one synchronous, since compiled East reads a lazy input without awaiting |
|
|
59
|
+
| `new InMemoryUnitIO(files?: Iterable<[path: string, bytes: Uint8Array]>)` | A `UnitIO` in memory, for a host with no file system: given every file the unit names, segments included; `.files` then holds them and every file the unit wrote |
|
|
60
|
+
| `UnitType` · `UnitResultType` | The unit and its result as East types, for a host that keeps them as beast2 |
|
|
61
|
+
|
|
62
|
+
- The unit's thread grant (`threads`) caps the frame pool for the rest of the
|
|
63
|
+
process: a grant of one frames every output inline.
|
|
64
|
+
- A unit whose host places segments as they are read (`fetch: true`) has
|
|
65
|
+
`io.segment` asked for each segment of a staged manifest before it is read.
|
|
66
|
+
`InMemoryUnitIO` holds every segment already, so its unit says `fetch: false`,
|
|
67
|
+
and a segment it does not hold fails at once, as any absent file does.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { East, IntegerType, InMemoryUnitIO, decodeBeast2For, encodeBeast2For, encodeEastIR, executeUnit, variant, type Unit } from "@elaraai/east";
|
|
71
|
+
|
|
72
|
+
const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
|
|
73
|
+
|
|
74
|
+
// A unit names its files by path: here, the program, its one input and its output.
|
|
75
|
+
const unit: Unit = {
|
|
76
|
+
work: variant("run", {
|
|
77
|
+
program: "program.beast2",
|
|
78
|
+
inputs: ["x.beast2"],
|
|
79
|
+
output: variant("value", "out.beast2"),
|
|
80
|
+
decode: variant("lazy", null),
|
|
81
|
+
}),
|
|
82
|
+
platforms: [], // the platform packages it needs, each resolved by `options.platforms`
|
|
83
|
+
threads: 1n,
|
|
84
|
+
fetch: false, // every segment it reads is in the IO already
|
|
85
|
+
result: "result.beast2",
|
|
86
|
+
};
|
|
87
|
+
const io = new InMemoryUnitIO([
|
|
88
|
+
["program.beast2", encodeEastIR(double.toIR())],
|
|
89
|
+
["x.beast2", encodeBeast2For(IntegerType)(21n)],
|
|
90
|
+
]);
|
|
91
|
+
const result = await executeUnit(unit, io, { platforms: () => [] });
|
|
92
|
+
// result.outcome: ok, or failed with its message and source locations; a failure is never thrown
|
|
93
|
+
decodeBeast2For(IntegerType)(io.read("out.beast2")); // 42n
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## IR → source: `East.toSource` and `east-node transpile`
|
|
97
|
+
|
|
98
|
+
Any IR — yours, or one exported from python — prints back as a module whose
|
|
99
|
+
`East.function(...)` rebuilds the same IR (equal under `east-c ir normalize`).
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
|
|
103
|
+
console.log(East.toSource(double));
|
|
104
|
+
// import { East, … IntegerType } from "@elaraai/east";
|
|
105
|
+
// export const main = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
east-node transpile double.beast2 -o double.ts # any runtime's IR
|
|
110
|
+
east-node transpile ./ir/ -o ./ts/ --rebuild ./rebuilt/ # a directory, and the IR each module builds back
|
|
111
|
+
east-py transpile double.beast2 -o double.py # the python twin
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- Statements print as the `$`-forms, a bound value as `$.let(value, T)`, an
|
|
115
|
+
Option as `some(v)` / `none`, other values as `East.value(v, T)`, builtins as
|
|
116
|
+
their methods; one with no method spelling prints as
|
|
117
|
+
`East.builtin(name, [T...], [args], out)`, which rebuilds. `East.as(v, T)` and
|
|
118
|
+
`East.wrapRecursive(v, T)` spell the `As` and `WrapRecursive` nodes.
|
|
119
|
+
- A platform call prints as its library spells it when you pass the library:
|
|
120
|
+
`East.toSource(fn, { libraries: { "@elaraai/east-node-io": await import("@elaraai/east-node-io") } })`.
|
|
121
|
+
Without it the call hoists to a declaration named after the function
|
|
122
|
+
(`const tar_create = East.asyncPlatform("tar_create", …)`). The printer finds
|
|
123
|
+
the handles in the module's exports — each handle `East.platform` returns
|
|
124
|
+
carries its `name`, `inputs`, `output` and `async`, and
|
|
125
|
+
`isPlatformDeclaration(x)` tells one from a function.
|
|
126
|
+
- Variables keep the names you wrote, read by the TypeScript compiler (an
|
|
127
|
+
optional peer); without it, or in a browser, they print as `_N`, as does a
|
|
128
|
+
slot the body did not name (`($, x) =>` for a `(value, index)` callback).
|
|
129
|
+
- The contract and the round-trip suites: `docs/conventions/EAST_CODEGEN.md`.
|
|
130
|
+
|
|
131
|
+
## Checking that a module builds: `east-node check`
|
|
132
|
+
|
|
133
|
+
`tsc` type-checks the TypeScript around a program, not the program: East's own
|
|
134
|
+
refusals — a body whose type differs from the declared output, a capture the
|
|
135
|
+
builder refuses, an `IRAnalysisError` — happen when `East.function` runs, at
|
|
136
|
+
import.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
east-node check dist/index.js # every broken function, at its line
|
|
140
|
+
east-node check dist/index.js --format json # the records `east-py check --format json` emits
|
|
141
|
+
east-node lsp # the diagnostics over stdio (needs @elaraai/east-diagnostics)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It reports **every** broken function, not the first. Importing a module runs
|
|
145
|
+
it, so `EAST_CHECK=1` is set meanwhile, and a module should skip import-time
|
|
146
|
+
work when it sees it. Edit-time idiom checks are separate:
|
|
147
|
+
`@elaraai/tsserver-plugin-east` in the editor, `@elaraai/eslint-plugin-east`'s
|
|
148
|
+
`east/east-rules` in `make lint`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Trees from flat rows, and recursion
|
|
2
|
+
|
|
3
|
+
`toTree`, `flattenTree`, and when recursion is safe. Part of the `east` skill.
|
|
4
|
+
|
|
5
|
+
## Trees from flat rows, and recursion
|
|
6
|
+
|
|
7
|
+
`toTree` builds nested data — any depth, your own node type — from rows that
|
|
8
|
+
name their key and their parent's key. `build` runs once per row, children
|
|
9
|
+
first; the result is the roots in source order.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
const Row = StructType({ id: StringType, parent: OptionType(StringType), name: StringType });
|
|
13
|
+
const Node = RecursiveType(self => StructType({ name: StringType, children: ArrayType(self) }));
|
|
14
|
+
|
|
15
|
+
const nest = East.function([ArrayType(Row)], ArrayType(Node), ($, rows) =>
|
|
16
|
+
rows.toTree(Node, ($, r) => r.id, ($, r) => r.parent, ($, r, _i, children) => ({ name: r.name, children })));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- A row whose parent is `none` is a root, and so is an orphan (its parent key
|
|
20
|
+
is not in the rows). A repeated key ❗ `toTree: duplicate key <key>`; a cycle
|
|
21
|
+
❗ `toTree: cycle through key <key>` — both before any `build`.
|
|
22
|
+
- A non-recursive `Node` folds bottom-up: `IntegerType` with
|
|
23
|
+
`children.sum().add(1n)` is each root's subtree size.
|
|
24
|
+
|
|
25
|
+
`flattenTree` is the way back: every node of nested data, each with its path
|
|
26
|
+
of keys (a Dict's) or indices (an Array's) from its root, depth first — each
|
|
27
|
+
node before its descendants, siblings in key or array order. `children` names
|
|
28
|
+
a node's children, of the collection's own type.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const Part = RecursiveType(self => StructType({ name: StringType, cost: FloatType, parts: DictType(StringType, self) }));
|
|
32
|
+
|
|
33
|
+
const flat = East.function([DictType(StringType, Part)], ArrayType(StructType({ path: ArrayType(StringType), name: StringType })), ($, parts) =>
|
|
34
|
+
parts.flattenTree(($, part) => part.unwrap().parts).map(($, e) => ({ path: e.path, name: e.node.unwrap().name })));
|
|
35
|
+
// [{ path: ["pump"], … }, { path: ["pump", "motor"], … }, { path: ["pump", "motor", "rotor"], … }, { path: ["pump", "seal"], … }]
|
|
36
|
+
|
|
37
|
+
// A rollup at every root: toTree over the paths, a node's parent its path less its last key
|
|
38
|
+
const totals = East.function([DictType(StringType, Part)], ArrayType(FloatType), ($, parts) =>
|
|
39
|
+
parts.flattenTree(($, part) => part.unwrap().parts).toTree(FloatType,
|
|
40
|
+
($, e) => e.path,
|
|
41
|
+
($, e) => East.greater(e.path.size(), 1n).ifElse(() => some(e.path.slice(0n, e.path.size().subtract(1n))), () => none),
|
|
42
|
+
($, e, _i, children) => children.sum().add(e.node.unwrap().cost)));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- The result is an Array of `{ path, node }`, not a Dict keyed by path: a Dict
|
|
46
|
+
key is immutable, and a path is an Array.
|
|
47
|
+
- `children` runs once per node, in the result's order. The walk is iterative;
|
|
48
|
+
each path is an array of its own, so a chain n deep holds n(n+1)/2 path
|
|
49
|
+
elements.
|
|
50
|
+
- **Recursion** — a function calling itself through a captured variable
|
|
51
|
+
(`$.let(fn, FunctionType(...))`, then `$.assign(fn, East.function(...))`) —
|
|
52
|
+
is for shallow depths, in the hundreds. Deeper, a call ❗
|
|
53
|
+
`call stack exhausted: East calls nested too deeply`, a catchable error on
|
|
54
|
+
every runtime. Walk deep data with `$.while`, `toTree` or `flattenTree`, which are iterative;
|
|
55
|
+
the C and Python runtimes still bound a recursive VALUE's depth to tens of
|
|
56
|
+
thousands of levels.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# East values in TypeScript
|
|
2
|
+
|
|
3
|
+
Host-side functions over East values (no expression, no IR), and building programs over names and types held as values. Part of the `east` skill.
|
|
4
|
+
|
|
5
|
+
## Values in TypeScript
|
|
6
|
+
|
|
7
|
+
Host-side functions for East values — build time, no expression, no IR. Each
|
|
8
|
+
takes the type and returns a function (`compareFor(T)(a, b)`).
|
|
9
|
+
|
|
10
|
+
| Signature | Description |
|
|
11
|
+
|-----------|-------------|
|
|
12
|
+
| **Comparing** |
|
|
13
|
+
| `compareFor(T)(a, b): -1 \| 0 \| 1` | East's total order — the order sets, dicts and `.sort()` use (NaN last) |
|
|
14
|
+
| `equalFor(T)`, `notEqualFor(T)`, `lessFor(T)`, `lessEqualFor(T)`, `greaterFor(T)`, `greaterEqualFor(T)` | The comparisons as East defines them: by value, NaN equal to itself and −0 apart from 0, variants by case name. Never `===`, `<` or a comparator of your own |
|
|
15
|
+
| `isFor(T)(a, b)` | East's `Is`: value for immutable types, identity for mutable containers — but two frozen collections by value, and a `Ref` always by identity |
|
|
16
|
+
| `new SortedMap(entries?, compareFor(K))` · `new SortedSet(values?, compareFor(K))` | `Map` / `Set` in East order — what decoders return |
|
|
17
|
+
| **Types** |
|
|
18
|
+
| `isValueOf(value, T)` | Whether a TypeScript value is a value of `T` |
|
|
19
|
+
| `printType(T)` · `isTypeEqual(a, b)` · `isSubtype(a, b)` | Print a type in East text; compare types |
|
|
20
|
+
| `diffTypes(actual, expected)` → `renderTypeDiff(diffs)` | The differences between two types, each with its path |
|
|
21
|
+
| `defaultValue(T)` · `minimalValue(T)` | A value of `T`: its default, and its least |
|
|
22
|
+
| **Codecs** — each `…For(T)` builds a codec for one type |
|
|
23
|
+
| `printFor(T)(value)` · `parseFor(T)(text)` | East text, the literal syntax `.east` files hold. `parseFor` returns `{ success, value \| error, position }`; `parseInferred(text)` returns `[type, value]` |
|
|
24
|
+
| `toJSONFor(T)(value)` · `fromJSONFor(T)(json)` | East JSON as a JavaScript value (`encodeJSONFor` / `decodeJSONFor` as bytes): lossless — an Integer a quoted decimal, a Blob hex, a flat Option (see JSON Schema) |
|
|
25
|
+
| `encodeCsvFor(StructT, config?)(rows)` · `decodeCsvFor(StructT, config?)(blob)` | An array of structs ↔ CSV bytes |
|
|
26
|
+
| `encodeBeast2For(T)(value)` · `decodeBeast2For(T)(blob)` | beast2, the binary form every runtime and e3 store — see below |
|
|
27
|
+
|
|
28
|
+
## Names and types held as values
|
|
29
|
+
|
|
30
|
+
East names a struct's fields and a variant's cases where its code is written:
|
|
31
|
+
`row.site`, `variant("load", x)`. Code that builds programs over types it is
|
|
32
|
+
handed — a library generating a program for whatever record its caller
|
|
33
|
+
declares — holds the names, and the types, as values. These build the same
|
|
34
|
+
program pieces from them, and check each name against the type as the program
|
|
35
|
+
is built: a name the type lacks fails there, naming the name and the type
|
|
36
|
+
(`East.field: .Struct {name, site, status} has no field 'job'`). Nothing in the
|
|
37
|
+
program differs from what the written form builds, so every runtime runs it.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const Machine = StructType({ name: StringType, site: StringType, status: StringType });
|
|
41
|
+
const Reading = VariantType({ load: FloatType, output: FloatType });
|
|
42
|
+
|
|
43
|
+
const site = "site"; // a name a caller hands you
|
|
44
|
+
const siteOf = East.function([Machine], StringType, ($, machine) =>
|
|
45
|
+
East.field(machine, site, StringType)); // typed by the type given, which is checked
|
|
46
|
+
|
|
47
|
+
const stopped = East.function([Machine], Machine, ($, machine) =>
|
|
48
|
+
East.withField(machine, "status", "down")); // the other fields as they are
|
|
49
|
+
|
|
50
|
+
const Place = StructType({ site: StringType, name: StringType });
|
|
51
|
+
const placeOf = East.function([Machine], Place, ($, machine) =>
|
|
52
|
+
East.struct(Place, East.fields(machine, ["site", "name"]))); // by name, or in the type's order
|
|
53
|
+
|
|
54
|
+
const readingOf = (place: string) => East.function([FloatType], Reading, ($, x) =>
|
|
55
|
+
East.inCase(Reading, place, x)); // a variant's case by name
|
|
56
|
+
|
|
57
|
+
const { diff, apply, compose, invert } = East.patchesFor(Machine); // East functions of a type held as a value
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A recursive value is read, and rebuilt, through its node. `fields` and
|
|
61
|
+
`withField` read the value once a field: give them a variable, not a
|
|
62
|
+
computation. Code whose names are written keeps writing `row.site`.
|