Echo remaining-feature priority
Current tagged version tracks 2.2.x. 1.1.0–1.1.9, 2.0.0–2.0.9, and 2.1.0–2.1.9 are complete; 2.2.0 ships elang ide; 2.2.1 hardens crash and path safety. Failure model stays abort + *Or.
User-facing docs prefer the CLI name elang (shells reserve echo). Historical slices below often say echo check / echo test / … — that is version history, not the command to type today.
The completed 0.5.x work was language basics: a few host/stdlib builtins plus two syntax extensions, then CLI/editor tooling. 0.5.9 is the last 0.5.x slice: echo check [paths...] with directory recursion (same as fmt / lint / test) plus editor Check workspace. 0.5.8 adds a small echo lint rule batch (test-naming, self-assign, unreachable-after-fail). 0.5.7 wires echo check / fmt / lint / test into the VS Code/Cursor extension as tasks and Problems matchers (not an LSP). 0.5.6 ships the native echo test product (expect* helpers, file/function units, summary). 0.5.5 ships fail(message) and echo lint. 0.5.4 ships echo fmt. 0.5.3 ships readFileOr, parseJsonOr, asIntOr, and asFloatOr. 0.5.2 makes the REPL keep session state across submissions. 0.5.1 hardened the 0.5.0 CLI (REPL continuation/quit, echo test semantics) and playground allow_run host enforcement.
Status values: pending / drafted / in progress / implemented (version) / held.
Implement now (0.5.x — completed)
| # | Item | Status |
|---|---|---|
| 1 | assert(cond, message) | implemented (0.5.0) |
| 2 | copyFile(src, dest) + pathJoin(...) | implemented (0.5.0) |
| 3 | run(command, args) | implemented (0.5.0) |
| 4 | now() — unix time as int seconds | implemented (0.5.0) |
| 5 | random() / randomInt(min, max) | implemented (0.5.0) |
| 6 | Multiline strings """ / ''' | implemented (0.5.0) |
| 7 | Number literals .5 and 1e3 / 1e-3 | implemented (0.5.0) |
| 8 | readLine() | implemented (0.5.0) |
| 9 | REPL (echo with no file) | implemented (0.5.2) |
| 10 | echo test product | implemented (0.5.6) |
| 11 | fail(message) | implemented (0.5.5) |
| 12 | echo lint | implemented (0.5.5), expanded (0.5.8) |
| 13 | expect / expectEq / expectNeq | implemented (0.5.6) |
| 14 | Editor tasks + problem matchers | implemented (0.5.7), Check workspace (0.5.9) |
| 15 | echo check [paths...] | implemented (0.5.9) |
1. assert(cond, message)
Abort with an Echo error if cond is falsy. message must be str. No Python AssertionError leaks. Analyzer: min 2 standalone args.
2. copyFile + pathJoin
copyFile(src, dest) is a UTF-8-agnostic binary copy of a file (not a directory). Restricted host (allow_files=False) denies with E2801. Missing src aborts. Missing dest parent aborts. Overwriting dest overwrites the file; abort if dest is a directory.
pathJoin takes 2+ string parts (variadic like say). Uses pathlib join, not string concat. Restricted host: pathJoin stays available (pure path strings; no files).
3. run(command, args)
command is str. args is a list of str (empty list allowed). Returns { "code": int, "stdout": str, "stderr": str }. No shell=True. Host.allow_run: bool = True by default; playground sets allow_run=False. Denied run → E2801-style “not available in this host”. Non-zero exit is not an Echo error. Missing executable is an Echo error.
4. now()
No arguments. Returns unix time as int seconds.
5. random / randomInt
random() → float in [0, 1). randomInt(min, max) inclusive integers, reject bool, require min <= max. Python random.
6. Multiline strings
Triple-quote """ and '''. Interpolation ${...} works, matching "..." / '...'. Newlines are allowed only in triples; single-line quotes stay single-line.
7. Number literals
.5 and scientific 1e3 / 1e-3. Keep existing 5. as integer then . (method calls like 5.asInt() stay valid).
8. readLine()
Read one line from stdin with no required prompt. EOF aborts with an Echo error.
9. REPL
echo with no file enters a REPL. Keep echo check and echo file.echo. --plain supported.
Harden 0.5.1: brace-depth and unterminated-triple continuation (... ), empty lines ignored, exit(code) / EOF quit without a traceback, one failed submission does not kill the process.
0.5.2 REPL session state (done): one Interpreter and Environment for the process. Analyzer-visible declarations persist across successful submissions, so a fn or variable defined at one echo> is usable at the next. Failed submissions do not drop earlier bindings. import loads sibling modules from the working directory into the session. use mut works for names declared earlier in the session.
10. echo test
echo test [paths...] is the native runner (0.5.6). No test DSL. No test "name" { } keyword.
A directory argument recursively runs *_test.echo files. An explicit file path always runs. No path prints help and exits 2. The first argv token test is the subcommand; echo test.echo still runs that file.
Zero-argument top-level fn testXxx() functions are separate units. Remaining top-level statements run once as setup, or as the file unit when there are no such functions. Exit 0 if every unit passed; 1 if any failed.
Harden 0.5.1: no path → help and exit 2; missing file → non-zero Echo error; exit(2) fails the unit; exit(0) passes; imported modules execute (unlike check).
11. fail(message)
Abort with an Echo error. message must be str. Same diagnostic family as assert (code E2825). Always abort. No *Or twin. Required message so scripts stay explicit.
12. echo lint
Style and convention findings, not a second typechecker. echo lint [paths...] walks files or recursive *.echo directories. Findings exit 1; clean exit 0; no path prints help and exits 2; parse errors match echo check. The first token lint is the subcommand; echo lint.echo still runs that file.
Rules: unused-local, unused-function, unused-import, comparison-to-bool, redundant-by-one, empty-block, shadow-builtin, test-naming, self-assign, unreachable-after-fail. Zero-arg fn test* entries are not unused-function (the test runner calls them). test-naming flags a top-level fn that looks like a test (test* / Test*) but is not a zero-arg testXxx unit. Unused parameters stay unused-local. unused-export is skipped (exports are for importers; no file-local convention). redundant-parens is skipped (the AST does not keep grouping parentheses).
13. expect / expectEq / expectNeq
Test-only continue-after-failure helpers. message must be str. expect(cond, message) requires cond to be bool. Under echo test a false/mismatch records and the unit continues. Outside echo test they abort like assert. assert / fail still abort. Codes E2826 / E2827 / E2828.
14. Editor tasks + problem matchers
VS Code / Cursor integration in echo-syntax-highlighter/: Run Task / command palette for elang check, elang fmt, elang lint, and elang test, with problem matchers into the Problems panel. Tasks use --plain. Echo: Check workspace (0.5.9) runs elang check --plain on the folder; Echo: Check file remains. Not an LSP: no completions, jump-to-definition, or language-server diagnostics.
15. echo check [paths...]
Last 0.5.x tooling slice. No new language syntax.
echo check [paths...] walks files or recursive *.echo directories, same collection helper as fmt / lint. Analyze only; do not run. Exit 0 if every file is clean; 1 if any parse/semantic check fails. After a failure, continue so echo check . reports every bad file, then exit 1. Missing path exits 1. No path prints help and exits 2. The first token check is the subcommand; echo check.echo still runs that file. --plain is supported.
0.6.x
0.5.9 closed the 0.5.x tooling arc. 0.6 opened language (functions as values, then collection helpers on those values). Historical note (written during the 0.6 planning window): at that time the then-current language version was described as 0.9.0. That sentence is not the package version today — see the top of this file and pyproject.toml for the current 2.1.x line. 0.6.x is complete. 0.7.0–0.7.9 are implemented. 0.8.0–0.8.9 are implemented. 0.9.0 is implemented.
0.6.0 shipped all three language items in one release:
- First-class functions including lambdas
- Slice syntax
- Default and variadic user
fnargs together
0.6.1 ships list map / filter on the function values from 0.6.0.
0.6.2 ships list reduce on those same function values.
0.6.3 ships list forEach on those same function values.
0.6.4 ships list flatMap on those same function values.
0.6.5 ships list some / every / findIndex on those same function values.
0.6.6 ships optional slice bounds xs[1:] / xs[:4] / xs[:] on the slice syntax from 0.6.0.
0.6.7 ships list zip / unique. (unique was originally queued as 0.6.8.)
0.6.8 ships four items: echo test -run, list chunk, rangeList / rangeListInclusive, and hash mapValues / filter.
0.6.9 ships four items: function types honor trailing defaults, list flatten, list partition, and echo test --json. Last 0.6.x slice.
Failure model is unchanged through 0.6.9: callbacks or default expressions that abort still abort. No try / catch. No Result / Option. No extra *Or twins in this stretch — the 0.5.3 set still covers the designed recovery cases.
Already shipped, so not re-proposed: args / env / files / JSON / fmt / lint / test / check / map / filter / reduce / forEach / flatMap / some / every / findIndex / zip / unique / chunk / rangeList / mapValues / hash filter / echo test -run / echo test --json / flatten / partition / function-type trailing defaults / slice() / xs[1:4] / optional slice bounds / lambdas / defaults / variadics / order(comparator) / find(value) / reverse / contains.
Held items below stay held through 0.7 (VM/JIT, classes, generics, async, packages, try/catch, Result/Option, overloading, LSP, dates/HTTP/regex, mkdir -p, test DSL). const / destructuring / exact objects / builtins-as-values / range-as-value are the 0.7 spine, not leftovers.
| Version | Item | Status |
|---|---|---|
| 0.6.0 | First-class functions including lambdas | implemented (0.6.0) |
| 0.6.0 | Slice syntax xs[1:4] | implemented (0.6.0) |
| 0.6.0 | Default and variadic user fn args | implemented (0.6.0) |
| 0.6.1 | map / filter | implemented (0.6.1) |
| 0.6.2 | reduce | implemented (0.6.2) |
| 0.6.3 | forEach | implemented (0.6.3) |
| 0.6.4 | flatMap | implemented (0.6.4) |
| 0.6.5 | some / every / findIndex | implemented (0.6.5) |
| 0.6.6 | Optional slice bounds xs[1:] xs[:4] xs[:] | implemented (0.6.6) |
| 0.6.7 | zip / unique | implemented (0.6.7) |
| 0.6.8 | echo test -run | implemented (0.6.8) |
| 0.6.8 | chunk | implemented (0.6.8) |
| 0.6.8 | rangeList / rangeListInclusive | implemented (0.6.8) |
| 0.6.8 | hash mapValues / filter | implemented (0.6.8) |
| 0.6.9 | Function types honor defaults | implemented (0.6.9) |
| 0.6.9 | flatten | implemented (0.6.9) |
| 0.6.9 | partition | implemented (0.6.9) |
| 0.6.9 | echo test --json | implemented (0.6.9) |
0.6.0 — first-class functions (including lambdas)
Functions are values. Lambda spelling: fn(x: int) -> int { ... } (inline => also works). Function type spelling: fn(int) -> int. order(comparator) accepts a function value, a lambda, or a name string.
0.6.0 — slice syntax
Spelling: xs[1:4] (colon, not ..). 0.6.0 required both bounds. 0.6.6 allows omitting either or both (xs[1:], xs[:4], xs[:]). Desugars to existing slice(start, end) with the same bounds as today: start and end in [0, length], end exclusive, no negatives, out-of-bounds aborts.
0.6.0 — default and variadic user fn args
User fn default arguments and variadic arguments ship together. Spelling: punct: str = "!" and parts: int... (variadic last, rest is a list of T). Defaults come before the variadic and are evaluated at call time. Overloading stays held. Function types still spell parameter types only (fn(int, str) -> int). 0.6.9 treats defaulted trailing parameters as optional when assigning.
0.6.1 — map / filter
List map(items, f) / filter(items, f) and method form items.map(f) / items.filter(f). filter requires a bool return (1 is not kept). Callbacks that abort abort the call. No string variants. Hash mapValues / filter ship in 0.6.8.
0.6.2 — reduce
List reduce(items, init, f) / items.reduce(init, f). f is fn(acc, item) -> acc. init is required; empty list returns init and does not call f. Returns a new value; does not mutate the input. A callback that aborts aborts the call. List only. No second fold builtin. Codes: non-function f E2832, wrong arity E2833, callback result type ≠ init E2834.
0.6.3 — forEach
List forEach(items, f) / items.forEach(f). Unary callback; the return value is discarded. Returns null. Does not mutate the input itself (the callback may, under use mut). Empty list is a no-op. A callback that aborts aborts the call. List only.
0.6.4 — flatMap
List flatMap(items, f) / items.flatMap(f). Unary f must return a list; a non-list return is a type error. Concatenates those lists one level into a new list. Empty input returns []. Does not mutate the input. A callback that aborts aborts the call. List only. Codes: non-function f E2837, wrong arity E2838, callback result not a list E2839.
0.6.5 — some / every / findIndex
List predicates on a unary fn(T) -> bool (same bool rule as filter). some is true if any element matches (empty → false). every is true if all match (empty → true). findIndex is the first matching index, or -1 if none — same sentinel as existing find(value). find(value) stays value search; do not overload it. Short-circuit. Method + standalone. List only. Callbacks that abort abort the call. Codes: non-function f E2840, wrong arity E2841, non-bool callback result E2831 (same as filter).
0.6.6 — optional slice bounds
xs[1:], xs[:4], and xs[:] parse and desugar to slice. Omitted start is 0; omitted end is length. Same abort rules as today for any bound that is present: [0, length], end exclusive, no negatives, out of range aborts. xs[:] is slice(0, length). Lists and strings, matching slice(). This is why 0.6.x does not also ship take / drop.
0.6.7 — zip / unique
Standalone zip(left, right) (and left.zip(right)) → a new list of 2-element lists [left[i], right[i]]. Both arguments must be list. Length is min(len(left), len(right)) (unequal is not an Echo error). Empty either side returns []. Does not mutate the inputs. Two lists only — no N-way zip, no zipper callback (that is map after zip, or later). Keywords left: / right:.
List unique(items) / items.unique() → a new list of first occurrences in original order, using Echo ==. Does not mutate the input. Empty → []. List only. Not reverse (already in-place on lists). Not hash take. Keyword items:.
0.6.8 — echo test -run, chunk, rangeList, hash mapValues / filter
Four items in one release. No new keywords. Failure model is unchanged.
echo test -run PATTERN (also --run) is a Go-shaped glob filter on function unit names (testXxx), not file names. *Add* matches testAdd; testAdd is exact. Non-matching units are skipped, not failed. A file with no matching units is skipped (not failed). Directories / *_test.echo still apply. --plain is unchanged. echo test.echo still runs that file.
chunk(items, size) / items.chunk(size) splits a list into new sublists of length size (int >= 1); the last chunk may be shorter. Empty list is []. Does not mutate the input.
rangeList(start, end) is exclusive-end, matching for i in start...end (step 1). rangeListInclusive(start, end) matches start..end. Empty when that for would not iterate. Returns list of int. Not 0...10 as a list value.
mapValues(h, f) / h.mapValues(f) returns a new hash with the same keys. Unary f(value) -> newValue. filter on a hash uses the same name as list filter and dispatches on the first argument / receiver type. f(value) must return bool true (not 1). Empty hash is empty hash. Insertion order is preserved. Callback abort still aborts. Does not mutate the input.
0.6.9 — function types honor defaults, flatten, partition, echo test --json
Four items in one release. Last 0.6.x slice. No new keywords. Failure model is unchanged.
Closes the 0.6.0 caveat: types still spell fn(int) -> int (no default markers, no ?). A function whose trailing parameters have defaults is assignable to a function type that omits those parameters, and still to the full-arity type. fn(x: int, y: int = 0) -> int matches fn(int) -> int and fn(int, int) -> int, not fn() -> int. Calls through the narrower type pass only those args; the implementation fills defaults at call time. Required parameters and variadics still have to match the type. Not builtins-as-values.
flatten(items) / items.flatten() concatenates one level of nested lists into a new list. Empty → []. A top-level non-list element is a type error. Does not mutate the input. Keyword items:.
partition(items, f) / items.partition(f) returns [matches, rest]. Unary f: fn(T) -> bool (same bool rule as filter: 1 is not ok). Empty → [[], []]. Callback abort still aborts. Does not mutate the input. Keywords items: / f:.
echo test --json writes a machine-readable JSON report to stdout. Totals include passed / failed / skipped (from -run). Each unit has name, pass/fail, and failure message + location when present. Composes with -run / --run and --plain. Human summary is off. Exit codes stay 0 / 1 / 2. echo test.echo still runs that file.
0.7.x
0.6.x is complete. 0.7 is the big-gun language series. Policy: one language-scale increment per version — syntax, type system, or value-model. Not chunk / zip-sized stdlib. Not another higher-order-function drip.
Spine (reserved for 0.7, not late 0.6): const, destructuring, exact object types, builtins as values, range as a value. Two more language items fit the same series: union types, then switch on values. String map / filter over graphemes stays skipped.
Failure model is unchanged through 0.7: abort by default; recovery is inquiry and the 0.5.3 *Or twins. No try / catch. No Result / Option. No ?. switch is value dispatch, not error handling (failure model).
Already shipped, so not re-proposed: everything in 0.5.x and 0.6.x (including rangeList / rangeListInclusive, function values for user fn / lambdas, open object aliases).
Held items below stay held through 0.7. Do not pull VM/JIT, classes, generics, async, packages, overloading, LSP, dates/HTTP/regex, mkdir -p, or a test DSL into this series.
Working spellings below are the plan of record so each version can be implemented without a second design pass. Open questions at the end of this section are the only knobs; changing one does not reorder the series.
| Version | Item | Status |
|---|---|---|
| 0.7.0 | const bindings | implemented (0.7.0) |
| 0.7.1 | Destructuring | implemented (0.7.1) |
| 0.7.2 | Exact object types | implemented (0.7.2) |
| 0.7.3 | Builtins as values | implemented (0.7.3) |
| 0.7.4 | Range as a value | implemented (0.7.4) |
| 0.7.5 | Union types | implemented (0.7.5) |
| 0.7.6 | switch on values | implemented (0.7.6) |
| 0.7.7 | Hash destructure rename | implemented (0.7.7) |
| 0.7.8 | Hash destructure rest | implemented (0.7.8) |
| 0.7.9 | Param const | implemented (0.7.9) |
0.7.0 — const bindings
Declaration-site immutability. Completes Echo's mutability story: use / use mut are capture; imports are Model A; const is the declaring-scope form.
Implemented spelling: const name: T = expr;. export const name: T = expr; is allowed. Types stay required (same as ordinary name: T =). The binding must be initialized. Reassignment of that name is a semantic error (E3201). use mut name; of a const binding is a semantic error (E3204). In-place collection mutation through that name (push, xs[i] = v, and other mutating methods) is rejected (E3202) — aligned with use, not JavaScript const.
The bound list or hash is frozen. Mutating that value through another name or a function parameter aborts (E3203). Reading / iterating / map (new list) is fine. Nested collections reached through a different mutable name are not deep-frozen. 0.7.9 adds const on parameters (fn f(const xs: list)).
Does not add a new runtime type. Ordinary (non-const) imported collections keep Model A identity. watch on a const name is legal and will simply never fire from that binding.
0.7.1 — destructuring
Unpack lists and hashes into names in one binding. This is new declaration / assignment syntax, not a stdlib helper.
Implemented spelling: [a: int, b: int] = pair; and { id: int, name: str } = user;. Types are required on a fresh declaration (Echo does not infer). Rest is last: [head: int, rest: int...] = xs binds rest as a list of T. Nested list patterns are allowed ([[a: int], b: int]). Compose with const: const [a: int, b: int] = pair;. Reassignment to already-declared names omits types: [a, b] = pair;. Same patterns in function parameters: fn f([a: int, b: int]) { } (the parameter type is list or hash).
Hash patterns bind listed keys; missing keys abort (E2711, same as user["x"]). Extra keys are ignored unless the source is an exact object type (0.7.2). Length mismatch on a list pattern without rest aborts (E3205). Wrong container type is E3206 / E3207. A callback or initializer that aborts still aborts.
Default: the field name is the binding name. 0.7.7 adds rename: { id as userId: int }. 0.7.8 adds hash rest: { id: int, rest: dynamic... }.
0.7.2 — exact object types
Closed hash shapes. Today type User = { id: int, name: str } requires those fields and allows extras. Exact types reject extras. This is the type-system counterpart of hashes-as-records, not structs or classes.
Implemented spelling: exact { id: int, name: str } as a type constructor, in aliases and inline (fn f(user: exact { id: int })). Open { id: int, name: str } stays open — not a breaking change. Aliases remain names, not nominal runtime types. Exact types require every listed key and type and reject extras at declarations, assignments, arguments, returns, destructured bindings, and foreach bindings. Exact → compatible open is assignable; open → exact is not. Extra fields are E3208 and missing fields are E3209. Exactness nests and does not freeze the hash.
Does not add methods on aliases. Does not add classes. dynamic still accepts any hash.
0.7.3 — builtins as values
Closes the 0.6 function-value hole. Named user functions are already values; say / map / filter and the other standalone builtins are assignable, passable, returnable function values with fn(...) types.
Implemented: f: fn(list, fn(dynamic) -> dynamic) -> list = map; is legal, as is fn(list, fn(int) -> int) -> list = map when the callback slot is the wide builtin type. Calls through that value are ordinary calls. Variadic builtins (say, eprint, format, pathJoin) are typed fn(dynamic...) -> void (say / eprint) or fn(dynamic...) -> str (format / pathJoin) — Echo has no null type, so void is the return. Dispatch builtins (filter on list vs hash) keep runtime dispatch on the first argument / receiver. Host-denied builtins remain values; calling them still aborts E2801. Method form without a call is also a value (xs.map); unknown properties still error E2704. Equality is the same builtin (interned name) or, for bound methods, the same builtin and the same receiver object. const bindings of a builtin alias cannot be reassigned; the builtin itself is not a mutable object.
Not overloading. Not a new Callable type. Function types still spell parameter types only (0.6.9 defaults rules unchanged).
0.7.4 — range as a value
start...end and start..end become expressions that produce a list of int, same bounds as for / rangeList / rangeListInclusive. 0...10 is [0, 1, ..., 9]; 0..10 is [0, 1, ..., 10]. Optional by step in expression position: 0...10 by 2. Eager list; no Range type. Empty when that for would not iterate. Non-numeric bounds and by 0 abort as they do in for.
for i: int in 0...10 stays the dedicated numeric loop (does not allocate a list). foreach i: int in 0...10 iterates the list value. Keep rangeList / rangeListInclusive; they are the same values as the exclusive / inclusive expressions. Slice syntax stays colon (xs[1:4]); xs[0...10] is indexing with a list and is a type error.
Implemented as above.
0.7.5 — union types
Runtime-checked unions as a typed alternative to dynamic. Implemented spelling: int | str in type position (|| stays boolean or). Flatten duplicates (int | str | int is int | str). int is assignable to int | str; int | str is not assignable to int. Object types, function types, and exact { ... } may be members. Lists stay untyped sequences (list); a binding may be list | hash.
Echo has no null type (null is only a dynamic value), so str | null does not parse — use str | void when a binding may hold null. Unions are not Option / Result: no ?, no unwrap, no error recovery. Control-flow narrowing (if type(x) == "int") is not implemented in 0.7.5.
Does not add generics, tagged enums, or exhaustiveness at compile time. Exhaustiveness belongs to switch (0.7.6) as a runtime/analyzer check on finite unions, not a new type-theory layer.
0.7.6 — switch on values
Value dispatch. This is control flow for literals, unions, and destructuring patterns — not error handling. Keyword is switch, not match, so match stays associated with the held Result / Option revision.
Implemented spelling (statement form; Echo is statement-based):
switch x {
0 { say("zero"); }
1 { say("one"); }
else { say("other"); }
}Arms: literal equality, type patterns (int { } / int n { }), and 0.7.1 destructuring patterns. else is required unless the arms cover a typed bool (true and false) or every member of a typed union via type patterns (E3210). A failed pattern does not abort the switch; it tries the next arm. No try. Aborting inside an arm still aborts. No expression-form switch in 0.7.6.
0.7.7 — hash destructure rename
Bind a hash key to a different local name. Completes a 0.7.1 open question; does not add hash rest.
Implemented spelling: { id as userId: int }. Key is id; binding is userId. Same-name stays { id: int }. Assignment omits types: { id as userId } = user;. Works in declarations, assignment, function parameters, and switch hash arms. Keyword as. { id: int as userId } is not the spelling.
0.7.8 — hash destructure rest
Collect leftover keys into a hash binding. Completes the other 0.7.1 open gap beside rename.
Implemented spelling: { id: int, rest: dynamic... }. Fixed fields bind as usual; remaining keys bind as a hash named rest. T... is the value type for each leftover entry; the binding’s type is always hash. Rest must be last. No as on the rest field. Assignment: { id, rest... } = user;. Empty leftovers are {}. Compose with rename: { id as userId: int, rest: dynamic... }.
0.7.9 — param const
Declaration-site immutability for parameters. Same rules as binding const.
Implemented spelling: fn f(const xs: list) { ... }, including lambdas, defaults, variadics (const xs: int...), and destructuring params (const [a: int, b: int]). Reassignment is E3201. In-place mutation through that name is E3202. use mut on a const param is E3204. The bound list/hash is frozen (shared identity with the argument).
0.7 open questions
These are spelling / tightness knobs. They do not add versions and they do not reopen Held.
| Topic | Working assumption | Alternatives |
|---|---|---|
const keyword | const name: T = expr; (locked in 0.7.0) | type-side name: const T = expr; |
const vs mutation | no reassignment and freeze of the bound list/hash (locked in 0.7.0); nested values via another name are not deep-frozen; param const locked in 0.7.9 | reassignment-only (JS-like); recursive deep freeze |
| Destructure types | types on fresh names; omitted on reassignment | always repeat types |
| Hash rename | { id as userId: int } (locked in 0.7.7) | { id: int as userId } |
| List rest | [head: int, rest: int...] | a ..rest token |
| Hash rest | { id: int, rest: dynamic... } (locked in 0.7.8); binding is hash; T... is value type; no as on rest | require hash... token; allow rename on rest |
| Exact objects | opt-in exact { ... }; open { ... } unchanged | #{ ... }; { ... }!; closed-by-default + ... rest (breaking) |
| Builtin function types | real fn(...) types, including variadics | dynamic callable with no signature |
Range by | 0...10 by 2 as an expression | step only in for; value form always step 1 |
| Union bar | int | str (locked in 0.7.5) | a or type keyword |
str | null | forbidden — no null type; use str | void for nullability (locked in 0.7.5) | invent a null type member |
| Switch keyword | switch | match (rejected: failure-model collision) |
| Switch narrowing | arm int { } / int n { } on a union | literals + else only |
0.8.x — OOP
Status: spine 0.8.0–0.8.2 implemented; polish 0.8.3–0.8.9 implemented.
0.7 is closed (spine through 0.7.6; polish through 0.7.9). Records stay hashes + exact { ... } + aliases. 0.8 adds nominal types with behavior — not “methods on type aliases,” and not classical Java (no inheritance-first design).
Shape: structs with methods, Echo-flavored.
- Nominal
classnames (runtime identity, not structural) - Fields are an exact shape (extras rejected)
- Methods use the existing call form
obj.method(args)(same surface as list/hash builtins) — 0.8.1 - Interfaces are capability types (method signatures) — 0.8.2
- Failure model unchanged: abort +
*Or; no exceptions from methods - Open
{ ... }hashes andexact { ... }keep working; classes do not replace them - Generics, overloading, async, packages, VM stay held outside this spine
- Public release: first PyPI upload of
echolangat 0.8.9 (not earlier)
Spine (done)
| Version | Item | Status |
|---|---|---|
| 0.8.0 | Nominal class + construction | implemented (0.8.0) |
| 0.8.1 | Methods + this | implemented (0.8.1) |
| 0.8.2 | interface (no inheritance) | implemented (0.8.2) |
Polish (draft / held until started)
Do not implement until explicitly started (start 0.8.3, etc.). Goal: make the OOP surface teachable and release-ready, then publish.
| Version | Item | Status |
|---|---|---|
| 0.8.3 | Explicit new { ... } field block | implemented (0.8.3) |
| 0.8.4 | Field defaults in new / construction | implemented (0.8.4) |
| 0.8.5 | Unbound methods Point.length(p) | implemented (0.8.5) |
| 0.8.6 | Type methods (no this) / Point.origin() | implemented (0.8.6) |
| 0.8.7 | Optional implements clause | implemented (0.8.7) |
| 0.8.8 | Docs / examples / README release pass | implemented (0.8.8) |
| 0.8.9 | First PyPI release (echolang) | implemented (0.8.9) |
0.8.0 — nominal class + construction
Introduce a nominal type distinct from type Alias = exact { ... }.
Implemented spelling (field block later moved under new { ... } in 0.8.3):
class Point {
new {
x: int;
y: int;
}
}
p: Point = Point { x: 3, y: 4 };
say(p.x);
p.x = 10;Rules:
class Name { new { field: T; ... } }declares a nominal typeName(as of 0.8.3)- Field list is exact: every field required; extras abort (E3208 / E3209)
- Construct with
Name { field: expr, ... }(named fields only; order free). No positionalPoint(3, 4)in 0.8.0 export class Name { ... }allowed, same as other top-level declarations- Nested classes: no in 0.8.0 (top-level only)
- Empty field list allowed (
class Marker { }) for nominal tags type(p)returns the class name string (e.g."Point"), not"hash"- Assignability:
Pointis only assignable toPoint(anddynamic). A compatibleexact { x: int, y: int }hash is not aPoint.Pointis not assignable to that exact type either — nominal, not structural Pointmay appear in unions (Point | str) and inswitchtype arms (Point { }/Point p { })- Field read/write:
p.xandp.x = v(typed). Unknown field → E2704 const p: Point = ...freezes the instance (E3202 through that name; E3203 via another name)- Destructuring:
{ x: int, y: int } = pworks (instances are hash-shaped for destructure) - Equality: field-wise Echo
==(like hashes), not identity - No methods, no
this, no inheritance, noprivatein 0.8.0
Does not add a separate struct keyword. class is the keyword; behavior is struct-like.
0.8.1 — methods + this
Attach functions to a class. Receiver is explicit as the first parameter named this.
Implemented spelling:
class Point {
new {
x: int;
y: int;
}
fn length(this) -> int {
return this.x * this.x + this.y * this.y;
}
fn move(this, dx: int, dy: int) -> void {
this.x = this.x + dx;
this.y = this.y + dy;
}
}
p: Point = Point { x: 3, y: 4 };
say(p.length());
p.move(1, 1);
bound: fn() -> int = p.length;
say(bound());Rules:
- Methods are declared inside the class body as
fn name(this, ...) -> T { ... }(or=>inline) - First parameter must be named
thisand has typeNameimplicitly (do not writethis: Point) - Call form:
p.length()—thisis bound top. Same surface asxs.map(f) - Method as value:
p.lengthis a boundfn(0.7.3 style). StandalonePoint.lengthas an unbound function is not required in 0.8.1 - Methods may read/write fields through
this - No static methods in 0.8.1 (
Point.origin()held) - No method overloading (held globally)
- Abort inside a method aborts the caller
- Analyzer: unknown method → member error; wrong arity → existing call errors
0.8.2 — interface (no class inheritance)
Capability types without single-parent inheritance.
Implemented spelling:
interface Named {
fn name(this) -> str;
}
class User {
new {
id: int;
label: str;
}
fn name(this) -> str {
return this.label;
}
}
fn show(n: Named) {
say(n.name());
}
u: User = User { id: 1, label: "Ada" };
show(u);Rules:
interface Name { fn method(this, ...) -> T; ... }— method signatures only, no fields in 0.8.2- A class implements an interface by defining every method with a compatible signature (structural match on the class’s methods). No
implementsclause required in 0.8.2 (inferred) - Assignability: class instance assignable to interface if it provides the methods. Interface to interface if the target’s methods are a subset
switchon interfaces: not exhaustive by class list;elsestill required unless other finite rules apply- No
extends/ class inheritance in 0.8. Shared behavior = interfaces + composition (store another instance as a field) - No default method bodies in 0.8.2
0.8.3 — explicit new { ... } field block
Make the constructor surface obvious inside class, without changing the call site.
Implemented spelling:
class Point {
new {
x: int;
y: int;
}
fn length(this) -> int {
return this.x * this.x + this.y * this.y;
}
}
p: Point = Point { x: 3, y: 4 };Rules:
- Fields live under
new { field: T; ... }(exact shape, same E3208 / E3209 rules) - Call site stays
Point { x: 3, y: 4 }(named fields only; order free) - Methods stay top-level in the class body (
fn …(this, …)); may appear before or afternew - Empty classes may omit
new(class Marker { }) or usenew { } - Bare top-level
x: int;insideclassis a parse error - At most one
newblock per class - Keyword is
new, notconstruct/init
Does not add ctor method bodies, positional Point(3, 4), or new Point(...).
0.8.4 — field defaults
Defaults on constructor fields so callers may omit them.
Implemented spelling:
class Point {
new {
x: int = 0;
y: int = 0;
}
}
origin: Point = Point {};
p: Point = Point { x: 3 };Rules:
- Default is an expression evaluated at construction time (same evaluation rules as fn defaults)
- Omitted fields use their default; required fields without defaults still E3209
- Extra fields still E3208
- Construction field values (provided or defaulted) are type-checked (E2001)
- Defaults do not change assignability of
Pointitself
Does not add optional types or null-default sugar beyond what expressions already allow.
0.8.5 — unbound methods
Use a method without an instance binding, by passing the receiver explicitly.
Implemented spelling:
class Point {
new { x: int; y: int; }
fn length(this) -> int {
return this.x * this.x + this.y * this.y;
}
}
p: Point = Point { x: 3, y: 4 };
say(Point.length(p));
raw: fn(Point) -> int = Point.length;
say(raw(p));Rules:
Point.lengthis an unboundfn(Point, …) -> T(first param is the class type)Point.length(p, …)is equivalent top.length(…)whenpisPoint- Bound form
p.lengthstays as in 0.8.1 - Unknown method on the type name → E2704
- If a variable shadows the class name,
.is ordinary instance member access
Does not add static/type methods without a receiver (that is 0.8.6).
0.8.6 — type methods (no this)
Methods that belong to the type, not an instance — factory / helpers.
Implemented spelling:
class Point {
new { x: int; y: int; }
fn origin() -> Point {
return Point { x: 0, y: 0 };
}
fn length(this) -> int {
return this.x * this.x + this.y * this.y;
}
}
p: Point = Point.origin();Rules:
- No
thisparameter ⇒ type method; call asPoint.origin() - May not read/write instance fields except through a constructed value
- Type method as value:
Point.originisfn() -> Point(or with its params) - Instance methods still require
thisas first param - Calling/binding a type method through an instance → E3213
- Duplicate method names still rejected (covers type/instance name clash)
Does not add free-standing static keyword. Absence of this is the signal.
0.8.7 — optional implements clause
Make intended interfaces visible at the class declaration; inference remains.
Implemented spelling:
interface Named {
fn name(this) -> str;
}
class User implements Named {
new {
id: int;
label: str;
}
fn name(this) -> str {
return this.label;
}
}Rules:
implements I, Jis optional. Omitting it keeps 0.8.2 inferred assignability- When present, the class must provide every listed interface’s methods (compatible signatures) or abort at analyze time (E3214)
- Multiple interfaces allowed; order free
- Still no class inheritance /
extends
Does not require implements for assignability (inference stays).
0.8.8 — docs / examples / README release pass
Implemented. No new language syntax. Public surface matches 0.8.0–0.8.7 before PyPI.
- README: remove stale “no classes”; install path ready for PyPI (
pipx install echolang/ preferelangoverecholang/echo) - Getting started + language reference:
new { }, defaults, unbound/type methods, optionalimplements - Capability matrix / known limitations / semantics notes through 0.8.7
- At least one small examples set covering class + interface
- Highlighter / VitePress keyword sync for
newin class-body context if needed - Confirm
./build.shbuilds a clean sdist+wheel; dry-run upload to TestPyPI encouraged - CLI: preferred command
elang(also registersecholangandecho)
0.8.9 — first PyPI release
Implemented (version + install docs). Publish echolang to PyPI when ready. Language surface is whatever 0.8.8 documented.
- Version 0.8.9 in
pyproject.toml/__init__.py/ highlighter package - Tag
v0.8.9(when releasing) - Upload via
./build.sh --release(or Trusted Publishing) after full pytest green - Docs install section points at PyPI; changelog marks first public package release
- Do not publish 0.8.0–0.8.8 to PyPI as the first public versions if avoidable — 0.8.9 is the intentional first upload
0.8 open questions
| Topic | Working assumption | Alternatives |
|---|---|---|
| Keyword | class (struct-like) — locked | struct |
| Construction call | Point { x: 3, y: 4 } (locked) + positional Point(3, 4) in 0.9.2 | Point.new(...) still held |
| Ctor field block | new { ... } — locked in 0.8.3 | keep bare fields forever |
| Receiver name | this — locked | self |
| Equality | field-wise == — locked | identity / === later |
| Unbound methods | 0.8.5 — locked | never |
| Type methods | 0.8.6 — locked (no this) | static keyword |
| Visibility | all public through 0.8.9 | priv later (held) |
implements | optional in 0.8.7 — locked; inference remains | required always |
| Inheritance | never in 0.8 | single extends later series |
| Class vs exact hash | strictly nominal | convert helpers later |
| First PyPI version | 0.8.9 (first upload) | wait for 1.0.0 |
| Stable public version | 1.0.0 | keep pre-1.0 forever |
Out of 0.8 (still held globally)
Inheritance trees, abstract classes, generics on classes, operator overloading, inner classes, and match / Result remain outside this spine. priv, member compound assign, and properties move to 0.9.x.
0.9.x — ergonomics / OOP polish
Status: 0.9.0–0.9.9 implemented; closed by stable release 1.0.0.
0.8 is closed. 0.9 is small language ergonomics from dogfooding — not inheritance, packages, generics, or a failure-model rewrite. Thin stdlib slices and one analyzer nit close the series.
Shape: finish day-to-day rough edges on the 0.8 OOP surface, then a short scripting/stdlib tail.
| Version | Item | Status |
|---|---|---|
| 0.9.0 | Compound assign on members (this.x += 1) | implemented (0.9.0) |
| 0.9.1 | priv / field–method visibility | implemented (0.9.1) |
| 0.9.2 | Positional construction Point(3, 4) | implemented (0.9.2) |
| 0.9.3 | Deep clone() (lists, hashes, class instances) | implemented (0.9.3) |
| 0.9.4 | Named format() placeholders via trailing hash | implemented (0.9.4) |
| 0.9.5 | Class properties get / set | implemented (0.9.5) |
| 0.9.6 | mkdirAll / removeTree | implemented (0.9.6) |
| 0.9.7 | Regex builtins | implemented (0.9.7) |
| 0.9.8 | Union narrowing in if type(...) | implemented (0.9.8) |
| 0.9.9 | Thin dates builtins | implemented (0.9.9) |
| 1.0.0 | Stable release (0.9.9 surface) | implemented (1.0.0) |
0.9.0 — compound assign on members
Same operators as simple-name compound assign (+= -= *= /= %=), on class fields.
Implemented spelling:
class Counter {
new {
n: int;
}
fn bump(this) {
this.n += 1;
}
}
c: Counter = Counter { n: 10 };
c.n *= 2;Rules:
- Target is
name.field(same surface asMemberAssignment); object is a simple name (this,c, …) - Semantics: read field → binary op → write field (same as
x += 1on locals) - Unknown field → E2704; non-class target → type error;
constinstance → existing const-mutation error - Does not add index compound assign (
xs[0] += 1) or chaineda.b.c += 1beyond one dot - Failure model unchanged
0.9.1 — priv
Class-private fields and methods. Keyword is priv (not private). Default remains public. No inheritance.
Implemented spelling:
class Counter {
new {
priv n: int = 0;
label: str;
}
priv fn bump(this) {
this.n += 1;
}
fn tick(this) {
this.bump();
}
}
c: Counter = Counter { label: "a" };
c.tick();Rules:
privon a field insidenew { ... }, or beforefnfor instance/type methods- Access only inside methods of the same class (by class identity); outside → E3215
- Construction may still name private fields to initialize them
- Private methods do not satisfy
implements/ interface assignability - No module-private or subclass-private (no inheritance yet)
0.9.2 — positional construction
Convenience call form alongside named Point { x: 3, y: 4 }. Not Point.new (held).
Implemented spelling:
class Point {
new {
x: int;
y: int = 0;
}
}
p: Point = Point(3, 4);
q: Point = Point(5);Rules:
Name(args...)whenNameis a class (and not shadowed by a variable/function) constructs an instance- Arguments bind to
new { ... }fields in declaration order - Trailing fields with defaults may be omitted; missing a required field → E3209
- Too many args or keyword args → E3216 (use named
Name { field: value }for kwargs) - Named construction remains fully supported and order-free
0.9.3 — deep clone()
clone() recursively copies nested structure.
Rules:
- Lists and hashes: deep copy; cycles reuse the in-progress clone (identity within one call)
- Class instances:
.clone()returns a new instance of the same class with deep-copied fields - Nested primitives are shared by value; unsupported top-level types still E2609
- Replaces the previous shallow-copy behavior
0.9.4 — named format() placeholders
Named placeholders resolve against a trailing hash argument (Echo has no kwargs).
Implemented spelling:
say("Hello, {name}!".format({ name: "Echo" }));
say("{0} scored {points}".format("Ada", { points: 42 }));
say(format("{greeting}!", { greeting: "Hi" }));Rules:
- Placeholders:
{},{0}/{1}/ …, or{identifier} - If the template uses any named placeholder, the last argument must be a hash
- Positional values are the preceding arguments (auto
{}and{n}index into those) - Missing trailing hash → E2306; missing key → E2307; invalid placeholder still E2302
- No width / precision / alignment specs
- Failure model unchanged
0.9.5 — class properties get / set
Computed fields with field-style read/write. Soft keywords (not reserved identifiers).
Implemented spelling:
class Counter {
new {
priv n: int = 0;
}
get count(this) -> int {
return this.n;
}
set count(this, value: int) {
this.n = value;
}
}
c: Counter = Counter {};
c.count = 10;
say(c.count);
c.count += 1;Rules:
get name(this) -> T/set name(this, value: T)— parallel tofn name(this)obj.nameinvokes the getter;obj.name = vand compound assign invoke the setter- Get-only or set-only allowed; missing side → E3217
priv get/priv setfor per-accessor visibility → E3215 outside the class- Name cannot clash with a field or method; get and set may share a name
- Does not add properties to interfaces; does not change construction (properties are not fields)
- Failure model unchanged
0.9.6 — mkdirAll / removeTree
Scripting filesystem gaps. Failure model unchanged.
Implemented spelling:
mkdirAll("out/nested/deep");
removeTree("out");Rules:
mkdirAll(path)creates parents; existing directory succeeds; file in the way → E2803removeTree(path)deletes a file or directory tree; missing path → E2802mkdir/removeFileunchanged- Restricted host (
allow_files=False) denies both → E2801 - No shell; no globbing
0.9.7 — regex builtins
Thin Python-re wrappers. No regex object type.
Implemented spelling:
say(regexMatch("hello 42", "\\d+"));
say(regexFind("hello 42", "\\d+"));
say(regexReplace("a1b2", "\\d", "X"));
say(regexSplit("a,b,c", ","));
say("abc123".regexFind("[0-9]+"));Rules:
regexMatch→ bool (search anywhere)regexFind→ match string ornullregexReplace→ replace all;\1-style groups in replacementregexSplit→ list of strings- Invalid pattern / bad types → E2850
- Failure model unchanged
0.9.8 — union narrowing in if type(...)
Today unions only narrow cleanly in switch type arms. Dogfood wants if type(x) == "int" { ... }.
Implemented:
- Narrow inside the
then(and matchingelse if type(...)) when the discriminant is a simple name elseexcludes the guarded union member- Static assign of a union to a narrower member without a guard is E2001
- Does not replace
switch; no full flow-sensitive typing - Failure model unchanged
0.9.9 — thin dates
Beyond now() / wait. Instant + format/parse enough for scripts — not a calendar product.
Implemented:
formatTime(secs, pattern) -> str/parseTime(text, pattern) -> int— UTC via Pythonstrftime/strptime; naive parse is UTCdays(n)/hours(n)/minutes(n)— seconds helpers- Bad input → E2851; no date object type; no cron / locale calendars
- HTTP moved to 1.1.x (drafted under 1.1.0)
1.0.0 — stable release
Implemented. No new language syntax. Public surface is 0.9.9. Failure model unchanged.
- Version 1.0.0; preferred CLI remains
elang - Marks the completed 0.8 + 0.9 work as the stable release line
- Next series is 1.1.x (drafted below)
Out of 0.9 (still held globally)
Inheritance, generics, packages, async, VM, try/catch, Result/Option, overloading, LSP-as-product, Point.new (positional construction shipped instead). HTTP builtins are drafted under 1.1.x, not held forever.
1.1.x — HTTP + post-1.0 polish
Status: 1.1.0–1.1.9 implemented (series complete).
1.0.0 is the stable cut. 1.1 is thin scripting/stdlib and tooling polish on that surface — not packages, generics, inheritance, or a failure-model rewrite. Lead with HTTP; then small helpers and diagnostics; close with docs sync / base64 / CLI inventory / series docs.
Shape: ship host-gated HTTP first, then headers / status helpers, optional tiny YAML or URL helpers, watch/abort polish, error-message pass, docs/playground policy; then 1.1.6–1.1.9 polish closeout (stdlib-only; no new PyPI deps).
| Version | Item | Status |
|---|---|---|
| 1.1.0 | httpGet / httpPost + allow_http host flag | implemented (1.1.0) |
| 1.1.1 | HTTP headers / status helpers / *Or twins | implemented (1.1.1) |
| 1.1.2 | YAML or URL helpers (only if still tiny) | implemented (1.1.2 — URL only) |
| 1.1.3 | watch / abort diagnostics polish | implemented (1.1.3) |
| 1.1.4 | Error-message / code pass | implemented (1.1.4) |
| 1.1.5 | Docs generator or playground HTTP policy | implemented (1.1.5 — playground host policy) |
| 1.1.6 | Thin docs generator / builtin sync | implemented (1.1.6) |
| 1.1.7 | base64Encode / base64Decode | implemented (1.1.7) |
| 1.1.8 | CLI builtin inventory polish | implemented (1.1.8) |
| 1.1.9 | Series close (docs / examples) | implemented (1.1.9) |
1.1.0 — httpGet / httpPost + allow_http
Implemented. Minimal HTTP for scripts. Stdlib/urllib only. No custom request headers yet (that is 1.1.1).
Spelling:
resp: hash = httpGet("https://example.com/");
say(resp["status"]);
say(resp["body"]);
posted: hash = httpPost("https://example.com/api", "{\"ok\": true}");Rules:
httpGet(url) -> hash { status: int, body: str, headers: hash }httpPost(url, body: str) ->same shapeHost.allow_httpdefaultTrue; playgroundFalse→ E2801 when denied- Network / URL errors → E2852; non-2xx status returns the hash (does not abort)
- No custom request headers in 1.1.0 (added in 1.1.1); urllib/stdlib only; no new PyPI deps
- Failure model unchanged
1.1.1 — HTTP headers / status helpers / *Or twins
Implemented. Extend the 1.1.0 surface without changing the response hash shape.
Rules:
- Optional trailing request headers hash on get/post (
httpGet(url, headers),httpPost(url, body, headers)) httpOk/httpRedirecton anintstatus or response hashhttpGetOr/httpPostOrtwins — network E2852 returns fallback; type errors and E2801 still abort- Still no new PyPI deps; failure model unchanged
1.1.2 — YAML or URL helpers (only if still tiny)
Implemented as URL helpers only. YAML deferred (no stdlib YAML; would need a dependency).
urlEncode/urlDecode/urlJoin/urlQuery- Bad types → E2853; urllib/stdlib only; failure model unchanged
1.1.3 — watch / abort diagnostics polish
Implemented. Tighten existing diagnostics around watch and abort paths. No new language syntax.
watchmutation lines includeat file:line:col- On abort, CLI /
echo testprint aWatched:dump of current watched bindings - Empty watch set adds no extra output
1.1.4 — error-message / code pass
Implemented. Consistency pass on Echo error messages and codes for the 1.1.x surface.
- Documented E2801 (host deny including HTTP) and E2850–E2853 (regex / dates / HTTP / URL) in errors-and-troubleshooting
- Documented
httpGetOr/httpPostOrin the failure-model twins table - Aligned URL helper and
days/hours/minutestype-error wording with HTTP / regex style - No new error codes; no failure-model change
1.1.5 — docs generator or playground HTTP policy
Implemented as playground HTTP / host-policy docs (not a docs generator). Complements allow_http from 1.1.0.
- Document CLI vs playground
Hostflags and empty env - Playground page + UI policy note; URL-helpers example that runs without network
- Worker defaults unchanged (
allow_files/allow_run/allow_httpallFalse) - Docs generator stays deferred to 1.1.6
1.1.6 — thin docs generator / builtin sync
Implemented. Generate/check the prelude builtin inventory from builtin_names() so docs and highlighters cannot drift.
tools/sync_builtins.py(+--check) updates:docs/reference/builtin-inventory.md- TextMate builtins match
- playground
echoLanguage.tsBUILTINSset
- Tests assert
--checkis clean and every builtin appears in built-in-methods - No new language syntax; no new PyPI deps
1.1.7 — base64Encode / base64Decode
Implemented. Thin stdlib base64 helpers for HTTP/scripting.
base64Encode(text)/base64Decode(text)— UTF-8 ↔ standard Base64- Method form on strings; bad types or invalid Base64 → E2854
- No new PyPI deps; failure model unchanged
1.1.8 — CLI builtin inventory polish
Implemented. Discoverability for the growing prelude.
elang builtins— sorted prelude names, one per line;--countprints the total- Top-level
-hepilog lists subcommands;-Valiases--version - Same names as
builtin_names()/ the generated inventory page
1.1.9 — series close (docs / examples)
Implemented. README + capability matrix + playground/examples pass for the full 1.1 surface. Closes 1.1.x.
- README / language tour / mini-programs cover HTTP, URL, Base64, and
elang builtins examples/url_and_base64.echo+ playground “URL & Base64” demo (no live HTTP)- No new language syntax; failure model unchanged
Out of 1.1 (still held globally)
Inheritance, generics, async, VM, try/catch, Result/Option, overloading, Point.new. Packages / stdlib modules are drafted under 2.0.x. YAML and an LSP stub are drafted under 2.1.x (full LSP-as-product stays held).
2.0.x — Modules beyond siblings + stdlib peel
Status: complete (2.0.0–2.0.9).
1.1.x is closed. 2.0 is the module-resolution and stdlib-module jump — not a package registry, not generics, not a failure-model rewrite. Sibling import name from "mod" stays valid. Failure model unchanged.
Shape: nested/relative paths first; then a fixed std/… search path; peel prelude families into std modules with a compatibility window; docs/examples close.
| Version | Item | Status |
|---|---|---|
| 2.0.0 | Relative / nested import paths ("./math", "lib/math") | done |
| 2.0.1 | Import cycle / resolver diagnostics polish | done |
| 2.0.2 | Stdlib search path: import … from "std/…" | done |
| 2.0.3 | Peel HTTP family into std/http (prelude dual-path) | done |
| 2.0.4 | Peel URL + Base64 into std/url / std/base64 (or std/net) | done |
| 2.0.5 | Peel files / JSON / env into std/fs + std/json | done |
| 2.0.6 | Peel regex + dates into std/re + std/time | done |
| 2.0.7 | Prelude policy: core vs std; optional require-std flag | done |
| 2.0.8 | Docs / examples / playground for multi-file + std/… | done |
| 2.0.9 | Series close (README / module-semantics / matrix) | done |
2.0.0 — relative / nested import paths
Done. Specifiers may include leading ./ / ../ and nested segments ("lib/math" → lib/math.echo beside the importer). Bare sibling names remain valid. Explicit .echo, absolute paths, and mid-path .. are E3001. Missing files E3002. Module identity is still the canonical absolute path. Selective import / explicit export unchanged. No registry. No import *.
2.0.1 — import cycle / resolver diagnostics polish
Done. Clearer module-not-found and cycle diagnostics for nested / relative path forms. E3002 reports the looked-for path and import location; E3001 carries specific hints; E3003 cycle chains use path labels that stay unique when basenames collide. Module identity remains the canonical file path.
2.0.2 — stdlib search path
Done. import … from "std/…" resolves from the Echo install / runtime tree (src/echo/std/, overridable via ECHO_STD_ROOT). Not the user’s cwd alone. "./std/…" remains a local relative path. Bare "std" is invalid. Seed module std/meta ships; no version pinning; no third-party packages. Builtin peels start in 2.0.3.
2.0.3 — peel HTTP into std/http
Done. Canonical imports: httpGet / httpPost / httpGetOr / httpPostOr / httpOk / httpRedirect from "std/http". Prelude still re-exports the same names for a compatibility window. Host allow_http unchanged. Method form and HTTP semantics unchanged.
2.0.4 — peel URL + Base64
Done. Canonical imports from "std/url" (urlEncode / urlDecode / urlJoin / urlQuery) and "std/base64" (base64Encode / base64Decode). Prelude dual-path unchanged. Separate modules (not a combined std/net).
2.0.5 — peel files / JSON / env
Done. Canonical imports: file/path helpers from "std/fs", JSON from "std/json", and env / envOr / args from "std/os". Prelude dual-path unchanged. Host allow_files / allow_run unchanged.
2.0.6 — peel regex + dates
Done. Canonical imports: regex helpers from "std/re", UTC date / duration helpers from "std/time". Prelude dual-path unchanged.
2.0.7 — prelude policy
Done. Core prelude (language / always-on helpers) vs peeled std/… families is documented. Optional Host.require_std and CLI --require-std hide peeled names from the prelude; default keeps today’s dual-path. std/… modules themselves still see the full builtin set.
2.0.8 — docs / examples / playground
Done. Multi-file and std/… coverage in examples and the playground: examples/std_imports.echo, examples/modules_demo/, playground std/… imports demo. Playground still denies files / run / HTTP; sibling multi-file projects remain CLI-only.
2.0.9 — series close
Done. README + module-semantics + capability matrix close the series. Public message: 2.0 = path resolution + std modules, not a package manager. No new language syntax; failure model unchanged. Next series: 2.1.x.
Out of 2.0 (still held globally)
Package registry / lockfiles / elang add, inheritance, generics, async, VM, try/catch, Result/Option, overloading, full LSP-as-product, Point.new. YAML and LSP stub wait for 2.1.x.
2.1.x — YAML + LSP stub + std maturity
Status: complete (2.1.0–2.1.9).
2.1 is tooling and deferred scripting polish on the 2.0 module surface — not a registry, not generics, not failure-model rewrite.
Shape: YAML (allowed PyPI dep), thin LSP, editor wire-up, light debugger polish, std/CLI inventory, docs close.
| Version | Item | Status |
|---|---|---|
| 2.1.0 | YAML helpers (yamlParse / yamlWrite or similar) | done |
| 2.1.1 | YAML *Or twin + codes / docs | done |
| 2.1.2 | Thin LSP stub (elang lsp / stdio) — hover + diagnostics | done |
| 2.1.3 | LSP goto-def for local + imported symbols | done |
| 2.1.4 | Editor extension: LSP client (Problems matchers remain fallback) | done |
| 2.1.5 | Debugger polish beyond watch (abort stack / trace) | done |
| 2.1.6 | Further std peel / leftover modules | done |
| 2.1.7 | CLI: elang std (or equivalent) listing std modules | done |
| 2.1.8 | Docs generator expansion (module + std inventory) | done |
| 2.1.9 | Series close (README / matrix / examples) | done |
2.1.0 — YAML helpers
Done. yamlParse(text) / yamlWrite(value) via PyYAML. Canonical import "std/yaml"; prelude dual-path (peeled under --require-std). Mappings → hashes, sequences → lists, whole numbers → int, other finite numbers → float, timestamps → ISO strings. Bad types / invalid YAML → E2855. No yamlParseOr yet (2.1.1).
2.1.1 — YAML *Or + docs
Done. yamlParseOr(text, fallback) returns fallback for invalid YAML; non-str text still aborts (E2855). Documented in the failure-model twins table and built-in methods. Exported from std/yaml.
2.1.2 — thin LSP stub
Done. elang lsp speaks stdio JSON-RPC. Publishes analyzer diagnostics on open/change; hover shows prelude builtin signatures. Full document sync only. Not a full IDE language server product.
2.1.3 — LSP goto-def
Done. textDocument/definition jumps to local bindings and to imported exports via the 2.0 module resolver (relative + std/…). Builtins have no source file (hover only).
2.1.4 — editor LSP client
Done. The VS Code / Cursor extension starts elang lsp (hover, goto-def, live diagnostics). Setting echo.lsp.enabled (default on). Task problem matchers and Check / Lint / Test remain as a fallback.
2.1.5 — debugger polish beyond watch
Done. Abort stack dump plus a small trace helper. No full stepper / breakpoints product.
- On abort, CLI /
echo testprintStack:(newest frame first) when the failure is inside a call; top-level aborts omit it trace(value)/value.trace()printsTRACE: …and returns the value unchanged- Works alongside existing
Watched:dumps from 1.1.3
2.1.6 — further std peel
Done. Leftover prelude families that clearly belong in std modules after 2.0.3–2.0.6 / YAML.
std/math—abs/floor/ceil/min/maxstd/random—random/randomInt- Extend
std/timewithnow/wait; extendstd/oswithrun - Prelude dual-path +
--require-stdunchanged (new names joinPEELED_STD_BUILTIN_NAMES)
2.1.7 — CLI std inventory
Done. elang std lists install-tree std/… modules; pairs with elang builtins.
- Default: one
std/nameper line (sorted); honorsECHO_STD_ROOT --count,--exports(indented export names),--path(resolved std root)- Top-level help epilog includes
std
2.1.8 — docs generator expansion
Done. Extend tools/sync_builtins.py for module / std inventory pages.
- Generates
docs/reference/std-inventory.mdfrom install-treestd/(module + exports) --checkreports std inventory drift alongside builtin / grammar / playground parser- VitePress Reference nav includes Std Inventory; cross-links with Builtin Inventory
2.1.9 — series close
Done. README + matrix + examples close the series. Public message: 2.1 = YAML + LSP stub + std maturity on 2.0 modules. No new language syntax; failure model unchanged. Example examples/yaml_and_math.echo + playground demo. Next work is outside 2.1 (package registry / full LSP still held).
Out of 2.1 (still held globally)
Package registry / lockfiles, inheritance, generics, async, VM, try/catch, Result/Option, overloading, full LSP-as-product, Point.new.
2.2.x — local tooling
Status: open (2.2.1 shipped).
2.2 is about running Echo without a terminal. Not a language change; failure model unchanged.
| Version | Item | Status |
|---|---|---|
| 2.2.0 | elang ide desktop editor (Tk) | done |
| 2.2.1 | Crash hardening + path safety | done |
2.2.0 — desktop IDE
Done. elang ide [file] opens a Tk window: editor with line numbers and Echo syntax highlighting, Run, and an output pane that separates stdout from stderr.
- Runs the unsaved buffer in-process through the normal pipeline; programs with
importsave first so the module loader reads the same text - Work happens on a background thread and results cross to the UI thread through a queue
- New package
echo.ide;runner.run_sourceis headless and unit-tested - Tkinter only (stdlib) — missing Tk prints an install hint and exits 1
- MVP: no autocomplete, no debugger, no project tree, no LSP in the window; interactive
ask/readLineare limited
2.2.1 — crash hardening + path safety
Done. Oversized integers, unbounded recursion, cyclic symlinks, and unsafe removeTree / removeFile paths abort as Echo errors instead of crashing or deleting the wrong tree. switch / hash-literal unescaping and elang fmt edge cases stay consistent. No new language syntax.
Held (do not implement)
Do not move global holds into a new series without an explicit series start. 0.8.0–0.8.9, 0.9.0–0.9.9, stable 1.0.0, 1.1.0–1.1.9, 2.0.0–2.0.9, and 2.1.0–2.1.9 are implemented. The closed 0.7 spine is in the 0.7.x table.
| Item | Status |
|---|---|
| Classes / OOP | spine 0.8.0–0.8.2 + polish 0.8.3–0.8.9 done |
Compound assign on members (this.x += 1) | implemented (0.9.0) |
priv / visibility | implemented (0.9.1) |
Positional construction Point(3, 4) | implemented (0.9.2) |
Deep clone() | implemented (0.9.3) |
Named format() placeholders | implemented (0.9.4) |
Class properties get / set | implemented (0.9.5) |
mkdirAll / removeTree | implemented (0.9.6) |
| Regex builtins | implemented (0.9.7) |
Union narrowing in if type(...) | implemented (0.9.8) |
| Thin dates builtins | implemented (0.9.9) |
| Stable release | implemented (1.0.0) |
| Generics | held |
| Async | held |
| VM / JIT | held |
| Packages / path resolution + std modules | 2.0.x complete; registry still held |
| YAML + LSP stub + std maturity | 2.1.x complete |
try / catch | held |
Result / Option | held |
match (error / Result form) | held — 0.7.6 is switch on values |
| User-level failure recovery | designed (failure model) — syntax held; *Or stdlib implemented (0.5.3) |
| Overloading | held |
Formatter / echo fmt | implemented (0.5.4) |
Linter / echo lint | implemented (0.5.5), expanded (0.5.8) |
Native test runner / echo test | implemented (0.5.6) |
Multi-path echo check | implemented (0.5.9) |
| Editor tasks + Problems matchers | implemented (0.5.7), Check workspace (0.5.9) |
| LSP-as-product | held — stub drafted (2.1.2–2.1.4) |
| YAML | drafted (2.1.0–2.1.1; needs a dependency) |
| HTTP builtins | 1.1.0–1.1.9 series complete |
Test DSL (test "name" { }) | held |
String map / filter over graphemes | skipped (awkward) |
Package registry / lockfiles / elang add | held (after 2.1) |
