Echo v0.4 boundary
Historical archive. Frozen design decision for the v0.4 cut only — not today’s surface. See Roadmap and Known Limitations.
Status: Host/stdlib implemented in v0.4.0. Theme: Host + standard library (not a syntax release) Contract: v0.4 standard libraryContracts that still win:
docs/language-semantics.md,docs/module-semantics.mdLater releases (0.5–0.8) added syntax and tooling after this freeze — see
CHANGELOG.md. The table below is what v0.4 decided to ship or hold at that time.
This boundary is settled for what v0.4 was. Changing the historical decision requires an explicit revisit; shipping later features did not rewrite this document’s purpose.
Frozen decision
v0.4 is a host-and-stdlib release. It is not a syntax release.
| Area | v0.4 decision | Later (not part of this freeze) |
|---|---|---|
| CLI arguments | Ship — builtin + CLI plumbing | Done |
| Environment variables | Ship — builtin | Done |
| File I/O | Ship — readFile / writeFile | Done (+ more host builtins) |
| JSON | Ship — parseJson / writeJson | Done (+ *Or) |
| String utilities | Ship | Done |
| Collection utilities | Ship | Done; HOFs in 0.6.x |
slice() | Ship — builtin | Done |
xs[1..4] / xs[1:4] | Hold — syntax sugar | xs[1:4] (+ optional bounds) in 0.6.x |
| Conversion semantics | Ship — semantic correction | Done; *Or in 0.5.3 |
| User-level failure handling | Design only | Failure model frozen; *Or twins shipped |
| First-class functions | Frozen (held then) | 0.6.0 |
| Modules / package paths | No change | Still sibling files |
| Formatter / test runner / REPL | No (held then) | REPL 0.5.0; fmt/lint/test afterward |
| VM / JIT / native / compiler | No | Still held |
| Generics / classes / async | No | Still held |
The load-bearing distinction:
nums.slice(1, 4) library capability
nums[1:4] language featureDo not add slice syntax because the method already exists. That is how a host release becomes a syntax expansion.
What “stdlib” means here
Echo has no package manager and no stdlib search path. v0.3 modules are sibling .echo files.
So “standard library” here does not mean import json from "std/json". It means more builtins: the same family as say, asInt, push, and order.
Those are implemented in the runtime method table, not in Echo source. That is still “not a language change.” New names are not new grammar.
Namespaced calls like file.read() look like a module object. Echo cannot do that without either packages-as-values or first-class modules. Use standalone names:
readFile("notes.txt");
writeFile("out.txt", text);
parseJson(text);
writeJson(value);
args();
env("HOME");Vision’s file.read() stays a later spelling if Echo ever gets a real package namespace. It is not a reason to add syntax now.
Three homes, used strictly
Builtin / runtime. New function or method. Lexer, parser, and semantic rules stay as they are. CLI may grow flags or leftover arguments.
Language semantics. No new tokens. Observable rules of existing operations are written down or changed. That is a language revision even when the spelling does not change.
Language design. New syntax, a new user-facing control-flow model, or a change to what a value is. Do not disguise this as “just a helper.”
Tier 1, one capability at a time
1. CLI arguments — builtin / runtime
Home. Runtime + builtin.
Why not language. A program receiving argv is host state, like stdin. ask already reads the host. args() can return a list of str.
Recommended shape.
flags: list = args();args()is process-global. Any module may call it.- The list is program arguments only, not the source path and not
--plain. - Do not inject a magic
argsbinding. That would be a new name-resolution rule.
CLI rule to decide later, not now. Interpreter flags stay on the Echo CLI. Program arguments begin after --, or after the source file once the flag grammar is specified. Example of the safe form:
echo app.echo --plain -- input.txt --verboseFailure. None for the happy path. Empty args() is a valid empty list.
Does not wait on. Failure handling, first-class functions, modules v0.4.
2. Environment variables — builtin
Home. Builtin.
Why not language. Same as args(): host state.
Recommended shape. Two functions, no optional parameters:
home: str = env("HOME");
city: str = envOr("CITY", "unknown");env(name)returnsstrand aborts if unset or empty — decide empty-vs-unset when specifying, not now.envOr(name, fallback)is the non-aborting form.- Two names beat one function with a default argument. User functions cannot have defaults; builtins should not grow a second parameter style without need.
pullalready has an optional index; do not add more of that casually.
Playground. Must not leak the builder’s machine environment. Return empty / abort / a fixed map. Specify with the playground host, not the grammar.
Does not wait on. A new error model. Unset env("X") is the same class of failure as a missing hash key.
3. File I/O — builtin
Home. Builtin. Vision already puts filesystem in a library, not the parser.
Why not language. Read and write are host operations. Echo already has method-call and standalone-call syntax.
Recommended shape. UTF-8 text only.
text: str = readFile("notes.txt");
writeFile("out.txt", text);Not in a first cut: append, directories, binary, glob, cwd helpers. exists can wait; a missing file is already an Echo error.
Failure. Missing file, permission, and invalid UTF-8 abort with Echo errors. That matches out-of-range indexes and missing hash keys. It does not require try/catch before files can exist.
Playground. Deny filesystem access with an Echo error. Do not silently no-op.
Does not wait on. User-level recovery. Files will motivate that design. They must not block on it.
4. JSON — builtin
Home. Builtin.
Why not language. Parse and write are conversions over existing values: object → hash, array → list, string → str, true/false → bool, null → null.
Recommended shape.
data: dynamic = parseJson(text);
text: str = writeJson(data);The one semantic choice. JSON numbers vs Echo int / float.
Recommendation: if the number is mathematically an integer, Echo int; otherwise Echo float. Reject non-finite values. Nested structures stay lists and hashes.
That choice belongs in the builtin spec, not in new syntax. It should be written down the same way int / int is written down.
Failure. Invalid JSON aborts with an Echo error. Same class as asInt("abc").
Does not wait on. Packages, first-class functions, or exceptions.
5. Better string operations — builtin
Home. Builtin methods on str.
Why not language. Echo already has trim, upperCase, format, and string indexing. split, replace, and contains are more of that list.
Recommended first cut.
parts: list = line.split(",");
clean: str = line.replace("foo", "bar");
ok: bool = line.contains("echo");split(sep)— separator must be a non-emptystr. Empty separator is an Echo error (do not copy Python’s character-split surprise).replace(old, new)— replace all non-overlapping occurrences. AreplaceFirstcan wait.contains(part)— Echo string containment. Emptypartistrue.
slice on strings shares the collection slice() rule below.
Not in the first cut. Regex, pad, locale case, format specifiers beyond {}.
Does not wait on. Language revision.
6. Better collection operations — builtin, except slice syntax
Home. Builtin methods. Slice syntax is language, and it is not required.
Why the split exists.
Echo can already express a slice without new grammar:
mid: list = nums.slice(1, 4);xs[1:4] or xs[1..4] needs the parser to accept a range inside []. Colon already means “type” and “hash key.” .. / ... already mean for-loop ranges. Either spelling is a real language change.
A method can be the desugaring target later. Syntax is optional sugar.
Recommended first cut.
ok: bool = nums.contains(3);
ok: bool = user.has("name");
mid: list = nums.slice(1, 4);- List/string
contains(value)uses Echo==. - Hash
has(key)tests key presence. Do not name thiscontains— key vs value is ambiguous. slice(start, end)returns a new list or string. End is exclusive.startin[0, length],endin[start, length].slice(0, xs.length())is a shallow copy of the sequence.- Out of range is an Echo index error. Do not clamp like Python. Echo indexes already error.
- No negative indexes. Echo list indexes are
[0, length). Do not grow a second indexing rule forslice. map/filter/reducewait on first-class functions. They are not part of this cut.
Does not wait on. A grammar change.
7. Tighten type conversions — semantic correction
Home. Existing language semantics. No new syntax.
This is not a new feature. It is a semantic correction: make the existing contract internally consistent.
The v0.2 contract says bool is not a subtype of int, and true is not a valid int. The implementation still does this:
asInt(true); // must become a type error
asInt(false); // must become a type errorTruthiness conversions stay:
asBool(0);
asBool("");
asBool([]);v0.4 needs no new conversion syntax. It writes the conversion rules down and removes the asInt(bool) backdoor.
Recommended contract.
| From → | asInt | asFloat | asBool | asString |
|---|---|---|---|---|
int | itself | n.0 | truthiness | decimal text |
float | truncate toward zero | itself | truthiness | Echo stringify |
str | parse whole string (trim ends); reject "" and "3.9" | parse whole string | truthiness | itself |
bool | type error | type error | itself | true / false |
null | type error | type error | false | null |
list / hash | type error | type error | truthiness | Echo stringify |
asBool stays explicit truthiness. That is a conversion people will call on purpose. asInt(true) is the one that should die.
For-loop bounds already reject bool and null. Conversion should match that taste.
When v0.4 is opened, this table moves into docs/language-semantics.md. Until then it is part of the frozen boundary, not live language law.
8. User-level failure handling — language design
Home. Language design. Not a builtin.
Why it is not stdlib. A helper that returns { ok: true, value: ... } is a convention, not a failure model. Every builtin would have to adopt it or none will. Index errors, missing keys, asInt("abc"), missing files, and invalid JSON are one family. They already abort.
Adding try/catch is a control-flow change. Adding Result / Option as real types is a type-system change. Either one is a language revision.
Frozen failure model for v0.4 host builtins.
operation
↓
success → value
failure → Echo-owned runtime error
↓
abort programNo try/catch because readFile() can fail. I/O can ship before recoverable errors without a conceptual mess.
If Echo later needs recovery, that is a separate language design:
operation
↓
success / failure-as-valueDesign that deliberately. Do not bolt it on in the same breath as files.
Stance.
- Ship host builtins with abort + Echo error, same as today.
- Write a failure-model design note in parallel. No syntax.
- Do not add
try/catchbecause Python and JavaScript have it. - Prefer studying failure-as-value (Go
error, RustResult) against Echo’s explicit taste. default()is truthiness fallback. It is not error handling. Do not overload it.
What the design note must answer before any syntax is proposed.
- Which failures are bugs (index
-1on a list) vs expected (file not found)? - Can a user function produce the same kind of failure a builtin produces?
- Is recovery in-language, or is “handle it in the caller by returning a hash” enough?
- What does
watchprint when a failure is handled?
Answered in docs/failure-model.md. Form: abort + inquiry + *Or twins. No try / catch. No Result type. *Or stdlib (readFileOr, parseJsonOr, asIntOr, asFloatOr) shipped in 0.5.3; syntax stays held.
I/O does not wait on this. If it did, Echo could never grow a host surface. The existing language already chose abort for expected runtime failures.
First-class functions — frozen, reconsider with evidence
Not implementing. Not closing forever.
Echo already has functions, closures, and one named function reference (order(comparator)). It does not have function → value. That boundary is unusual. It is also written in the v0.2 contract.
Keep it frozen for any first v0.4 cut.
Put it on a reconsider with evidence list, not a “never” list.
Evidence that would justify reopening:
- A stdlib API that cannot be expressed without a function value, beyond
order - Repeated
foreach+pushin real Echo programs afterslice/containsexist - A need to return a nested function from a factory (
makeCounter)
Evidence that should not reopen it:
- “Python has lambdas”
- “We want
mapandfilter” said before non-HOF collection helpers exist
Doing slice / contains / split first reduces pressure for functions-as-values. That is useful. It lets the later discussion happen with examples, not with a hole in the checklist.
What a first v0.4 cut is, and is not
Is
args,env,envOrreadFile,writeFileparseJson,writeJsonsplit,replace,contains,has,slice- A semantic correction:
asInt(bool)/asFloat(bool)become type errors;asBoolkeeps truthiness - A failure-handling design note with no syntax (abort model stays)
Is not
xs[1:4]/xs[1..4]try/catch/Resultsyntax- Function values, lambdas,
map/filter echo fmt,echo lint,echo test, REPL, LSP- Packages,
file.read()namespaces - VM, JIT, generics, classes, async
Tier 2 tooling can proceed in parallel. It does not define v0.4. Tier 3 stays behind a hard line.
Dependencies
args, env, strings, contains, slice()
→ no blockers
readFile, writeFile, parseJson
→ playground host policy
→ not blocked by failure-handling syntax
conversion contract
→ explicit language revision when v0.4 opens
→ may break asInt(true) / asFloat(true)
failure handling
→ design only
→ informed by file / json / env abort behavior
first-class functions
→ frozen
→ reopen only with evidence after the non-HOF stdlib existsOpening v0.4
This file does not open v0.4.
When that work is started on purpose, follow the v0.3 sequence:
- Contract — write the v0.4 language/stdlib contract. Conversion rules move into
docs/language-semantics.md. Builtin names and abort behavior are specified. Slice syntax, function values, andtry/catchstay out. - Tests — lock the contract with tests before implementation.
- Architecture — CLI argv plumbing, playground host policy, builtin table.
- Implementation — host + stdlib only.
Do not start at step 4 from this discussion.
The boundary in one sentence
v0.4 adds host power and missing methods. It corrects asInt/asFloat on bool. It designs, and does not ship, user-level failure handling. It does not add syntax.
