Skip to content

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.md

Later 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.

Areav0.4 decisionLater (not part of this freeze)
CLI argumentsShip — builtin + CLI plumbingDone
Environment variablesShip — builtinDone
File I/OShipreadFile / writeFileDone (+ more host builtins)
JSONShipparseJson / writeJsonDone (+ *Or)
String utilitiesShipDone
Collection utilitiesShipDone; HOFs in 0.6.x
slice()Ship — builtinDone
xs[1..4] / xs[1:4]Hold — syntax sugarxs[1:4] (+ optional bounds) in 0.6.x
Conversion semanticsShip — semantic correctionDone; *Or in 0.5.3
User-level failure handlingDesign onlyFailure model frozen; *Or twins shipped
First-class functionsFrozen (held then)0.6.0
Modules / package pathsNo changeStill sibling files
Formatter / test runner / REPLNo (held then)REPL 0.5.0; fmt/lint/test afterward
VM / JIT / native / compilerNoStill held
Generics / classes / asyncNoStill held

The load-bearing distinction:

text
nums.slice(1, 4)     library capability
nums[1:4]            language feature

Do 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:

echo
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.

echo
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 args binding. 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:

text
echo app.echo --plain -- input.txt --verbose

Failure. 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:

echo
home: str = env("HOME");
city: str = envOr("CITY", "unknown");
  • env(name) returns str and 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. pull already 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.

echo
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.

echo
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.

echo
parts: list = line.split(",");
clean: str = line.replace("foo", "bar");
ok: bool = line.contains("echo");
  • split(sep) — separator must be a non-empty str. Empty separator is an Echo error (do not copy Python’s character-split surprise).
  • replace(old, new) — replace all non-overlapping occurrences. A replaceFirst can wait.
  • contains(part) — Echo string containment. Empty part is true.

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:

echo
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.

echo
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 this contains — key vs value is ambiguous.
  • slice(start, end) returns a new list or string. End is exclusive.
  • start in [0, length], end in [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 for slice.
  • map / filter / reduce wait 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:

echo
asInt(true);    // must become a type error
asInt(false);   // must become a type error

Truthiness conversions stay:

echo
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 →asIntasFloatasBoolasString
intitselfn.0truthinessdecimal text
floattruncate toward zeroitselftruthinessEcho stringify
strparse whole string (trim ends); reject "" and "3.9"parse whole stringtruthinessitself
booltype errortype erroritselftrue / false
nulltype errortype errorfalsenull
list / hashtype errortype errortruthinessEcho 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.

text
operation

success → value
failure → Echo-owned runtime error

        abort program

No 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:

text
operation

success / failure-as-value

Design that deliberately. Do not bolt it on in the same breath as files.

Stance.

  1. Ship host builtins with abort + Echo error, same as today.
  2. Write a failure-model design note in parallel. No syntax.
  3. Do not add try/catch because Python and JavaScript have it.
  4. Prefer studying failure-as-value (Go error, Rust Result) against Echo’s explicit taste.
  5. 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 -1 on 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 watch print 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 + push in real Echo programs after slice / contains exist
  • A need to return a nested function from a factory (makeCounter)

Evidence that should not reopen it:

  • “Python has lambdas”
  • “We want map and filter” 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, envOr
  • readFile, writeFile
  • parseJson, writeJson
  • split, replace, contains, has, slice
  • A semantic correction: asInt(bool) / asFloat(bool) become type errors; asBool keeps truthiness
  • A failure-handling design note with no syntax (abort model stays)

Is not

  • xs[1:4] / xs[1..4]
  • try / catch / Result syntax
  • 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

text
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 exists

Opening v0.4

This file does not open v0.4.

When that work is started on purpose, follow the v0.3 sequence:

  1. 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, and try/catch stay out.
  2. Tests — lock the contract with tests before implementation.
  3. Architecture — CLI argv plumbing, playground host policy, builtin table.
  4. 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.

Echo is in active development. The docs reflect the current implementation.