@elaraai/east-claude-plugin 1.0.90 → 1.0.91

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elaraai/east-claude-plugin",
3
- "version": "1.0.90",
3
+ "version": "1.0.91",
4
4
  "description": "East programming language ecosystem - a statically typed, expression-based language for business logic, data processing, UI definitions, and durable execution",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -98,10 +98,12 @@ platform = East.platform_functions(__name__) # pass to East.compile(fn, platfo
98
98
  A body's parameters are expressions already. A python scalar or `datetime` inside
99
99
  a body lifts to a literal; `East.value(v, T)` / `b.const(v)` lift anything else
100
100
  (TS `East.value` / `$.const`); an East COLLECTION an `East.function` body closes
101
- over is snapshot into the IR (`.bind(table)` it as a trailing parameter to keep it
102
- live). The other way, an artifact is a plain callable on values, every eager
103
- method accepts one (a VALUE takes no block), and one called inside another body
104
- is a call of its inline Function node — see [Python values vs East expressions](#python-values-vs-east-expressions).
101
+ over is snapshot into the IR as a literal of its contents, rebuilt wherever the
102
+ body reads it — `t = b.const(table)` builds it once per call, and `.bind(table)`
103
+ as a trailing parameter keeps it live. The other way, an artifact is a plain
104
+ callable on values, every eager method accepts one (a VALUE takes no block),
105
+ and one called inside another body is a call of its inline Function node — see
106
+ [Python values vs East expressions](#python-values-vs-east-expressions).
105
107
 
106
108
  ## Decision Tree: What Do You Need?
107
109
 
@@ -136,7 +138,7 @@ Task → What do you need?
136
138
  │ ├─ Call python from a body → the @East.platform_function you hold IS the Platform node (no declaration) · East.platform /
137
139
  │ │ asyncPlatform / genericPlatform only for one implemented elsewhere · East.compile(fn, platform=East.platform_functions(__name__))
138
140
  │ │ → "Declare, implement, build, compile", "Platform functions"
139
- │ ├─ Make it fast → data stays in East · chain · CSE by reusing a python variable · capture small tables, .bind big ones
141
+ │ ├─ Make it fast → data stays in East · chain · bind shared work with b.const · b.const a captured table, .bind big ones
140
142
  │ │ → "Performance — the levers, ranked"
141
143
  │ ├─ IR ↔ python → to_python_source · east-py transpile · compile_from_beast2 / json / east / value · east-c ir … → "IR ↔ python"
142
144
  │ ├─ Refusals at edit time → east-py lint · flake8 --select EAS · east-py lsp; TYPE errors → east-py check
@@ -167,9 +169,10 @@ Task → What do you need?
167
169
  │ └─ Let East call python → @East.platform_function · @East.generic_platform_function · @memoize → "Platform functions"
168
170
  │
169
171
  ├─ C. Cross between them → a body's parameters are expressions · python scalars and datetimes lift · East.value(v, T) /
170
- │ b.const(v) · a captured East collection is a build-time SNAPSHOT (an eager callback refuses it), .bind(table) keeps it
171
- │ live · an East function is a callable on values, and a VALUE takes no block · east.runtime.compiler.eager_stats()
172
- │ shows how a hot call ran → "Python values vs East expressions", "Performance"
172
+ │ b.const(v) · a captured East collection is a build-time SNAPSHOT, a literal rebuilt at each read (an eager callback
173
+ │ refuses it) — b.const(table) builds it once per call, .bind(table) keeps it live · an East function is a callable on
174
+ │ values, and a VALUE takes no block · east.runtime.compiler.eager_stats() shows how a hot call ran
175
+ │ → "Python values vs East expressions", "Performance"
173
176
  │
174
177
  └─ D. Run a program from the shell → east-py run (IR + input files → a result) · exec (a unit: the runner protocol e3
175
178
  speaks) · convert (a beast2 value → East text) · version · where its time goes: --profile / EAST_PROFILE=1
@@ -309,12 +312,12 @@ artifact used as an `East.if_else` arm evaluates only when that arm is taken,
309
312
  an error inside it names its own lines, and a profile lists it as an entry of
310
313
  its own, placed where it was written and where it was called. (One called with
311
314
  arguments narrower than its parameters — a column projection's narrowed rows —
312
- re-runs its source in its own frame and splices in instead, as does one whose
313
- build hoisted constants.) An artifact that mutates or raises, called and
314
- DISCARDED, is the build's "evaluated and thrown away" error — spell it
315
- `b.do(push(xs))` / `East.block(push(xs), result)`, as with any mutation. An
316
- artifact that calls platform functions stays first-class (composable,
317
- serializable) but raises until `East.compile` pairs it with implementations.
315
+ re-runs its source in its own frame and splices in instead.) An artifact that
316
+ mutates or raises, called and DISCARDED, is the build's "evaluated and thrown
317
+ away" error — spell it `b.do(push(xs))` / `East.block(push(xs), result)`, as
318
+ with any mutation. An artifact that calls platform functions stays first-class
319
+ (composable, serializable) but raises until `East.compile` pairs it with
320
+ implementations.
318
321
  `East.function(...)` spelled INSIDE a body is the inline `Function` node, a
319
322
  Function-typed expression: bind it with `b.const`, hand it to a callback slot,
320
323
  or call it (a `Call`; in an `East.asyncFunction`, calling an async one is a
@@ -512,7 +515,7 @@ East values. A worked example is in
512
515
  | `East.while_(state, cond, body, label=…)` | `While` | `cond(b, s) -> Boolean`, `body(b, s) -> next state` |
513
516
  | `East.for_(coll, state, body, label=…)` | `ForArray`/`ForSet`/`ForDict` | Array `body(b, s, el[, i])`, Set `body(b, s, el)`, Dict `body(b, s, k, v)` |
514
517
  | `East.block(a, b, …)` | `Block` | evaluates in order, yields the last — the sequencing point for mutators (`East.block(lambda b: …)` is the statement form) |
515
- | `East.let(value, fn)` | `Let` | `fn(b, bound)` — bind once, use many times (explicit CSE inside a loop) |
518
+ | `East.let(value, fn)` | `Let` | `fn(b, bound)` — bind once, use many times, inside an expression body (`b.const` is the statement form) |
516
519
  | `East.ref(v)` | `NewRef` | a cell — `.get()` / `.update(v)` / `.merge(v, fn(b, current, patch))` |
517
520
  | `East.label(name=None)` | — | names a loop, for `break_`/`continue_` from a NESTED one |
518
521
  | `East.break_(state=…, label=…)` / `East.continue_(state=…, label=…)` | `Break` / `Continue` | leave / next iteration, optionally committing a last state |
@@ -535,10 +538,10 @@ counts = East.for_(rows, {"counts": East.new_dict(StringType, IntegerType)},
535
538
  ```
536
539
 
537
540
  ⚠️ Seed accumulators with `East.new_array/new_set/new_dict`. A captured
538
- `EastArray(T)` works too — a loop's seed is built fresh per call — but
539
- anywhere ELSE in a function a captured collection is a build-time snapshot
540
- shared by every call, and mutating one raises rather than leaking state
541
- between calls.
541
+ `EastArray(T)` works too — a captured collection is a snapshot of its
542
+ contents, rebuilt wherever the body reads it, so a seed is fresh per call —
543
+ but mutating one anywhere ELSE changes a copy nothing else sees, and the
544
+ build raises rather than compile a mutation that persists nowhere.
542
545
 
543
546
  ### Queries — `East.jq`
544
547
 
@@ -621,34 +624,39 @@ element**.
621
624
  crossing per element — convert at most once, at the very end.
622
625
  2. **Let the callbacks build, and chain on the results** — a chain of
623
626
  `map`/`filter`/`reduce` stays native between steps. Return a dict literal
624
- (`lambda b, r: {"a": …, "b": …}`) to derive every column in ONE pass. **Reuse
625
- a python variable to share work** (build-time CSE): `fields =
626
- r.data.split("|")` read for 30 columns compiles to ONE `Let` — the split runs
627
- once per row (re-calling `.split()` per column re-emits it). Loop-invariant
628
- subexpressions hoist out of nested lambdas to the function body, a value read
629
- once inside a callback included, so `table = derive(rec)` before a `.map` runs
630
- once. A hoist never leaves a guard: a subexpression shared inside `b.try_` /
631
- `East.try_catch` binds inside that body, so `s.try_parse(T)` still answers
632
- `none` when CSE'd. `East.function(..., cse=False)` switches the pass off and
633
- builds exactly what the body spells (what the transpiler emits).
634
- 3. **Side tables: capture small ones, `bind` big ones** — opposite contracts:
627
+ (`lambda b, r: {"a": …, "b": …}`) to derive every column in ONE pass. **Bind
628
+ shared work with `b.const`** — the builder emits exactly what the body
629
+ spells, as TypeScript's does, so a python variable holding an expression is
630
+ re-emitted (and re-evaluated) at every read: `fields = r.data.split("|")`
631
+ read for 30 columns runs the split 30 times per row, and `table =
632
+ derive(rec)` read inside a `.map` runs `derive` once per element. `fields =
633
+ b.const(r.data.split("|"))` and `table = b.const(derive(rec))` run each
634
+ once, as `$.const` does — the `no-reinlined-east-binding` rule (`east-py
635
+ lint`, the LSP) points at every such read. There is no optimisation pass
636
+ (#1312 removed the trace-time CSE: it moved reads across mutations it could
637
+ not see), so what you write is what runs — a callback that pushes onto
638
+ `order` and returns `order.size() - 1` reads the size after its push.
639
+ 3. **Side tables: `b.const` a captured one, `bind` big ones** — opposite contracts:
635
640
  - **Capture (snapshot).** A collection captured by an `East.function` body
636
- is snapshot into its IR — hoisted and identity-deduped, so it builds **once
637
- per compiled function** and each lookup runs in C. Right for tables up to
638
- ~10⁴–10⁵ entries; the snapshot's build cost and memory ride the function (a
639
- 1M-entry dict takes ~10 s), and later mutations are **not** seen. Only an
641
+ is snapshot into its IR as a literal of its contents — exactly as a
642
+ TypeScript literal is — and rebuilt wherever the body reads it: per call
643
+ at the function's top level, per element inside a callback. **Bind it
644
+ once per call with `t = b.const(TABLE)`** and read `t`. Right for small
645
+ tables (up to a few thousand entries): the literal's size rides the IR,
646
+ every call rebuilds it, and later mutations are **not** seen. Only an
640
647
  EXPLICIT build snapshots: an eager callback refuses a mutable capture.
641
648
  - **`East.function(...).bind(table)` (by reference, live).** Declare the
642
- table as a trailing parameter and pre-bind it: zero copy, O(1) at any size
643
- (1M entries: ~0.1 ms), the per-row cost of the hoisted case, and the
644
- function **sees later mutations**. Rebinding gives independent callables,
645
- the unbound function stays usable, and a wrong-typed value raises
646
- `TypeError`.
649
+ table as a trailing parameter and pre-bind it: zero copy, O(1) at any
650
+ size, nothing rebuilt per call, and the function **sees later
651
+ mutations**. Rebinding gives independent callables, the unbound function
652
+ stays usable, and a wrong-typed value raises `TypeError`.
647
653
 
648
654
  ```python
649
655
  fx = EastDict(StringType, FloatType, rates) # small table → capture
650
- to_usd = East.function([Row], FloatType, # (the EXPLICIT build
651
- lambda b, r: r.amount * fx.get_or_default(r.ccy, 1.0)) # opts into snapshot)
656
+ @East.function([Row], FloatType) # (the EXPLICIT build
657
+ def to_usd(b, r): # opts into the snapshot)
658
+ t = b.const(fx) # the literal, once per call
659
+ return r.amount * t.get_or_default(r.ccy, 1.0)
652
660
 
653
661
  TableT = DictType(StringType, FloatType) # huge table → bind
654
662
  conv = East.function([Row, TableT], FloatType,
@@ -680,7 +688,7 @@ REBUILDS it through the surface above:
680
688
 
681
689
  ```python
682
690
  from east.codegen import to_python_source
683
- print(to_python_source(classify)) # `@East.function(..., cse=False)` / `def main(b, …)`
691
+ print(to_python_source(classify)) # `@East.function(...)` / `def main(b, …)`
684
692
  ```
685
693
 
686
694
  ```bash
@@ -789,6 +797,7 @@ The rules — `east-py lint --list-rules` prints them:
789
797
  | `no-python-string-building` (EAS023, warning) | an f-string assembling an East string CONSTANT — a regex, a template, a key | spell the constant out |
790
798
  | `no-derived-struct-fields` (EAS024, warning) | `Derived = StructType([… for f in Other.value])` | a type declaration is a wire format — spell the fields |
791
799
  | `no-python-data-work` (EAS025, warning) | a python helper doing parse / strip / null-check / coerce work on an EXPRESSION a body hands it | express it in East, where it runs on every row |
800
+ | `no-reinlined-east-binding` (EAS026, warning) | a python local holding an EXPRESSION (`n = xs.size()`, not a `b.let`/`b.const` binding) read more than once in one body, or read inside a per-element callback, a loop body or a nested function below the body that bound it | each read re-emits — and re-evaluates — the expression: bind it once, `n = b.const(xs.size())`, where it is needed (the TypeScript rule of the same name) |
792
801
 
793
802
  Configure it once in the project's `pyproject.toml`; every surface reads it, and
794
803
  `east-py lint`'s own flags add to it:
@@ -811,10 +820,7 @@ A file that does not import `east` is never diagnosed; a line ending in `# noqa`
811
820
  `node_modules`, `dist`, `build`, `.git`, `__pycache__`, `tests` and `test` are not
812
821
  walked. The rules are syntactic, so a clean `lint` is necessary, not sufficient —
813
822
  the type errors live behind `east-py check`. The rules are disjoint (each
814
- mistake is one rule's to report). One TypeScript rule has no python twin on
815
- purpose: `no-reinlined-east-binding` warns that a JS `const` holding an `Expr`
816
- re-inlines it at each use, but the python build's CSE binds a reused python local
817
- to ONE `Let`, so the hazard does not exist here.
823
+ mistake is one rule's to report).
818
824
 
819
825
  ### Cross-language functions: `east-py export-functions` and `East.import_function`
820
826
 
@@ -833,12 +839,11 @@ from east.types.types import FloatType, StructType, IntegerType
833
839
  Row = StructType([("qty", IntegerType), ("price", FloatType)])
834
840
  score = East.function([Row], FloatType, lambda b, r: r.qty.to_float() * r.price)
835
841
  east_functions = {"score": score} # name -> East.function artifact. CLOSED values: the
836
- # exported IR must BIND every name it reads. A build's
837
- # own hoisted constants — a captured lookup table, a
838
- # stdlib format string — ride along and close over
839
- # nothing; a .bind result (no IR of its own) and
840
- # an unresolved import are refused, and a genuinely free
841
- # variable is refused BY NAME
842
+ # exported IR must BIND every name it reads. A captured
843
+ # lookup table or a stdlib format string is a literal in
844
+ # the body and closes over nothing; a .bind result (no IR
845
+ # of its own) and an unresolved import are refused, and a
846
+ # genuinely free variable is refused BY NAME
842
847
  ```
843
848
 
844
849
  ```typescript
@@ -1635,7 +1640,7 @@ CUTOFF = 100
1635
1640
  @East.function([Row], FloatType)
1636
1641
  def usd(b, r):
1637
1642
  return East.if_else(r.amount > CUTOFF, # a scalar constant lifts (TS East.value)
1638
- r.amount * RATES.get_or_default(r.ccy, 1.0), # an EXPLICIT build snapshots the dict
1643
+ r.amount * RATES.get_or_default(r.ccy, 1.0), # an EXPLICIT build snapshots the dict: a literal, rebuilt per call
1639
1644
  0.0)
1640
1645
 
1641
1646
  rows.map(lambda b, r: r.amount * RATES.get_or_default(r.ccy, 1.0)) # ❌ an eager callback REFUSES