Skip to content

Full self-hosting parity, differential testing, MIT license, flagship demo#1

Merged
MettaMazza merged 51 commits into
mainfrom
feat/self-hosting-parity
Jul 4, 2026
Merged

Full self-hosting parity, differential testing, MIT license, flagship demo#1
MettaMazza merged 51 commits into
mainfrom
feat/self-hosting-parity

Conversation

@MettaMazza

Copy link
Copy Markdown
Owner

What this delivers

Ernos is now fully self-hosting with zero caveats, MIT-licensed, and ships a flagship demo.

Self-hosting (the headline)

  • The self-hosted compiler (epc, written in Ernos: lexer, parser, semantic checker, optimizer, codegen) compiles 53/53 runnable test programs and rejects 12/12 compile-error programs — full parity with the Rust reference compiler.
  • Byte-identical 3-stage fixpoint (gen2 == gen3).
  • Clang-only bootstrap: bootstrap/epc_bootstrap.c builds a working compiler with no Rust anywhere in the chain, verified by bootstrap/verify.sh (freshness-checked in CI).
  • Single shared C runtime (runtime/ep_runtime.c) embedded by both compilers — generational GC, coroutine async scheduler, no runtime drift.

Hardening from adversarial differential testing

38 novel programs compiled by BOTH compilers with outputs diffed caught and fixed:

  • a silent not (a > b) miscompilation (C precedence),
  • grammar drift on single-line closures,
  • closure-name collisions in the Rust codegen,
  • three type-safety holes (literal return-type mismatch, string arithmetic, unchecked call-argument types) — now hard errors in both compilers,
  • a float-in-f-string bug that printed raw double bits,
  • error-message hints that suggested nonsense ("Did you mean 'for'?") — now targeted plain-English guidance.

The differential suite is permanent (tests/run_differential.sh) and runs in CI alongside parity, fixpoint, and the Rust-free bootstrap on macOS + Linux.

Performance

Numeric functions with declared Int/Float/Bool params skip GC bookkeeping entirely: fib(35) went 0.19s → 0.03s user — identical to clang -O2 C.

New in this PR

  • LICENSE: MIT — free for anyone, for any purpose.
  • Flagship demo examples/bakery.ep (The Plainville Bakery): ~300 lines exercising the whole language — structs, traits, enums + check, try/Result, closures, custom iterators, async event loop, threads + channels, floats, f-strings, maps, file I/O, SHA-256. Both compilers produce byte-identical output for it (CI-enforced).
  • README/AGENT.md truth-up: accurate line counts, gate tables, honest measured benchmark.

Verification (all green)

  • ./run_tests.sh71/71
  • bash tests/run_epc_parity.sh53/53 runnable + 12/12 rejected, 0 wrongly accepted
  • bash tests/run_differential.sh38/38 agree
  • bash tests/run_fixpoint.sh — byte-identical
  • bash bootstrap/verify.sh — clang-only, Rust-free, green
  • ASAN-clean on GC stress tests via both compilers; zero cargo warnings

🤖 Generated with Claude Code

MettaMazza and others added 30 commits July 1, 2026 22:45
…9 rejections)

tests/run_epc_parity.sh compiles every runnable tests/*.ep with the SELF-HOSTED
compiler (./epc), runs the binary, and diffs stdout against .expected. Expected-
compile-error tests form a rejection section (epc must reject them once the
self-hosted semantic passes land).

Baseline measured on this commit: PASS 13/54 runnable; 5 rejected / 4 wrongly
accepted. Also handles epc's CWD output-path divergence (tracked for Phase 4)
and cleans up generated artifacts. .gitignore gains !tests/*.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eterministic

- RUNTIME_HEADER_AND_SRC (4708 lines of C) moves to runtime/ep_runtime.c,
  embedded via include_str! — byte-identical by construction. This is the
  single-source-of-truth runtime both compilers will embed (self-hosted side
  lands next).
- Fix nondeterministic emission: six codegen loops iterated a HashMap when
  declaring locals / pushing GC roots, so the SAME compiler binary produced
  different C run-to-run (verified: 3 distinct hashes in 4 runs). Locals are
  now emitted in sorted order; 4/4 emissions hash-identical after the fix.
  Prerequisite for reproducible builds and the ernos==epc byte-parity gate.

run_tests.sh 69/69; self-hosting gate passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-identical)

concat, int_to_string, str/ptr helpers, StringBuilder, read_line/read_int,
GC counters, and float conversions move from inline push_str calls to
runtime/ep_builtins.c embedded via include_str!. Emitted C verified
hash-identical before/after (70c4909c…). Second of the two shared-runtime
files the self-hosted compiler will embed.

run_tests.sh 69/69; self-hosting gate passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orms)

The scan bases come from char locals (&_marker / &_top_marker), so the void** walk
could start misaligned -- undefined behaviour that skews the scan window on strict
platforms (caught by valgrind on Linux; macOS/arm64 tolerated it silently). Mask the
base DOWN: (uintptr_t)lo & ~(uintptr_t)7 in ep_gc_scan_own_stack_minor, and the same
for *ep_thread_tops[t] in the major-path ep_gc_scan_thread_stacks. Aligning down only
widens the conservative window by a few harmless bytes; aligning up could skip the
slot holding a live root.

Diagnosis trail (external session, credited): five falsified hypotheses -- WASM setjmp
stub, register-only roots, unregistered main-thread bottom, init frame offset, hash-
table dropout -- before valgrind + instrumentation exposed the odd scan window.

Validated: 141-suite Smithian Fold corpus regenerated and green (826 checks), ex-
crashers stressed 20x clean, heavy bisection proofs bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… GC)

The self-hosted compiler's emitted runtime was the OLD primitive collector
(no-op push_root, single generation). It now embeds the same runtime text as
the Rust compiler, via ep_runtime_gen.ep — generated from runtime/ep_runtime.c
+ runtime/ep_builtins.c by tools/gen_runtime_ep.ep (written in ErnosPlain).

- get_c_runtime_source (1,462 lines of hand-maintained C-in-.ep) now delegates
  to get_shared_runtime_source(); round-trip verified byte-identical (169,133
  chars) against the source files.
- Renamed ep_codegen.ep's .ep-level helpers ep_int_to_str/string_contains to
  cg_* — they collided with the C builtins of the same names once the full
  runtime arrived.
- This also delivers wholesale the builtins epc was missing (concat, string
  family, math/time, crypto, FFI dlopen/dlcall, fs, StringBuilder, float
  conversions…).

Gates: 3-stage fixpoint byte-identical with the new runtime; parity 13→15/54;
run_tests.sh 69/69 (Rust side untouched); tests/test_gc_remembered_uaf.ep
(4-thread old→young churn) compiled BY EPC runs ASAN-clean, exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y (parity 15->24/54)

f-strings were the single largest parity blocker (66 'undeclared identifier f'
errors across the suite): the self-hosted lexer had no notion of them and
emitted 'f' as a bare identifier.

- ep_lexer.ep: new lex_string_body handles all string literals. F-strings are
  desugared directly in the token stream, mirroring the Rust parser's semantics:
  f"a{x}b" -> concat("a" and concat(ep_auto_to_string(x) and "b")) via a
  streaming right-fold. Interpolated expressions are lexed by calling
  tokenize_source recursively (brace-depth + nested-string aware) and splicing
  the tokens. Adds \{ and \} escapes.
- ep_codegen.ep: register return types for ~70 shared-runtime builtins
  (string family, math/time, crypto, files, FFI dlcall*, bytes, map/list, GC
  introspection) so display/type-inference handle their results correctly.
  Dropped map_has_key (documented in AGENT.md but implemented nowhere).

Gates: 3-stage fixpoint byte-identical; parity 15->24/54; run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k flags (parity 24->26/54)

- is_global_var misclassified '_' (no lowercase letters) as an ALL_CAPS global
  constant, so 'set _ to ...' emitted an assignment to an undeclared C
  identifier. Globals now additionally require an uppercase letter.
- User/imported .ep functions that shadow a C-runtime builtin (e.g. stdlib
  string.ep wrappers vs the shared runtime's string_length) are no longer
  emitted — the builtin wins, mirroring the Rust compiler's builtin_c_funcs
  skip. Registry snapshot stored as state slot 10 (builtin_count).
- epc's clang command now passes -DEP_HAS_SQLITE with -lsqlite3 (the runtime's
  sqlite wrappers are ifdef-gated) and OpenSSL flags for crypto.ep.

Gates: fixpoint byte-identical; parity 24->26/54; run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the NODE_CLOSURE stub ('0 /* closure not yet implemented */') with the
Rust compiler's EpClosure ABI:

- Capture analysis via new collect_idents_expr/collect_idents_stmts walkers:
  identifiers read in the body that are outer variables (excluding params,
  known functions, ret_val), deduplicated.
- Lifted function long long _ep_closure_N(long long _ep_env, params...) with
  captures unpacked from ((long long*)_ep_env)[i]; generated via buffer
  swapping on the emit list and spliced into a /* EP_CLOSURE_BODIES */
  placeholder line after the prototypes (state slot 11).
- Creation site mallocs {magic, fn_ptr, env[]} and packs capture values.
- Indirect calls through variables use magic-number dispatch (EpClosure vs raw
  function pointer), so raw fn ptrs and closures share call sites.
- Bare function names as values now emit (long long)name (HOF arguments).

Gates: fixpoint byte-identical; parity 26->31/54 (test_closures,
closure_capture/locals/indirect/foreach, hof_named now pass); run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r compare (parity 31->32/54)

is_builtin_c_func and the closure capture filters compared strings with
'x equals y' where x had unknown type — the codegen only emits strcmp when the
LEFT operand is known to be a string, so these compiled to pointer comparisons
that were always false (the extern-prototype guard silently never fired,
leaving conflicting string_length/... redeclarations). Coerce via
string_concat(x and "") first — the same idiom map_get/map_put use.

Gates: fixpoint byte-identical; parity 31->32; run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed fixpoint gate

The previous commit made is_builtin_c_func actually work, which exposed three
registry entries that are NOT C-runtime symbols (string_concat is a compiler-
internal .ep helper; float_to_string / ep_time_ms don't exist in the shared
runtime). Their .ep definitions were suppressed by the new skip, breaking
self-compilation (undeclared string_concat) — and the ad-hoc fixpoint check
compared stale binaries, masking the failure.

- Remove the three bogus registry entries (verified every remaining entry
  against runtime/*.c symbol definitions).
- tests/run_fixpoint.sh: proper gate that checks every stage's exit code and
  runs a basic-math smoke test on the fixpoint binary.

Gates: FIXPOINT OK (3-stage, rc-checked); parity 32/54; run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ency

- ep_lexer.ep: describe/let/be/show/print/loop/every/returns/stop/skip/times
  keyword aliases and the 'give back' multi-phrase (mirrors src/lexer.rs).
- ep_parser.ep: keyword tokens double as variable names in expression position
  (mirrors the Rust parser's expect_identifier leniency) — required so sources
  using loop/skip/show/etc. as variables keep compiling now that those words
  lex as keywords. Set targets already used the token value.

test_english_aliases still has a tail of phrase-level aliases (comparison
phrases, tiered forms) — tracked, not yet passing.

Gates: FIXPOINT OK; parity 32/54 held; run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ep_parser.ep: optional 'as <alias>' after the import path; import entries
  become [path, alias] pairs.
- epc.ep: aliased modules are parsed into fresh lists, then merged with BOTH
  original names (intra-module calls) and alias_-prefixed duplicates (importer-
  facing), mirroring the Rust driver's flattening.

Gates: FIXPOINT OK; parity 32->33/54 (test_import_alias); run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(parity 33->34/54)

'set d to North' / 'Leaf' etc. parsed as plain identifiers and emitted as
undeclared C names. Variant names are now registered during the tag-define
walk (state slot 12); NODE_IDENT resolves them to the zero-payload enum
construction (same emission as NODE_ENUM_CREATE with no args). Locals shadow
variant names.

Gates: FIXPOINT OK; parity 33->34/54 (test_bst, test_recursive_enum);
run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bool literals and 'not' expressions now infer TYPE_BOOL (7); display emits
(v) ? "true" : "false" instead of %lld, matching the Rust compiler.
Comparison/logical results stay int-typed to avoid arithmetic-context
regressions.

Gates: FIXPOINT OK; parity 34->35/54 (test_core); run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delivers the 'fully self-contained' milestone. bootstrap/epc_bootstrap.c is epc
compiled by epc (the fixpoint C output, 19,580 lines). From it the whole
toolchain builds with only clang:

  bash bootstrap/build.sh    # clang epc_bootstrap.c -> epc, then epc rebuilds epc.ep

bootstrap/verify.sh proves self-containment: clang-only build -> 3-stage
byte-identical fixpoint from the frozen C (Rust never invoked) -> freshness
check (committed artifact must match what epc.ep produces today) -> epc parity
suite. All green.

The Rust compiler is now needed only for LSP + cross-language transpilers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uctions

README: real self-hosting state (fixpoint, shared runtime, bootstrap/build.sh +
verify.sh for the Rust-free build). AGENT.md: stronger self-hosting gates
(run_fixpoint.sh, run_epc_parity.sh), shared-runtime regeneration rule, and the
bootstrap freshness rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ep_lexer.ep: float literal tokenization (digits '.' digits -> TOK_FLOAT 70).
- ep_parser.ep: NODE_FLOAT (42) carrying the literal text.
- ep_codegen.ep: NODE_FLOAT emits a double punned into long long; NODE_BINARY
  detects float operands and unpacks/computes/repacks (matching the Rust ABI);
  infer_type returns TYPE_FLOAT (8) for float literals and float arithmetic;
  display uses %.15g for floats; int_to_float and ep_dlcall_f* registered as
  float-returning.
- Regenerated bootstrap/epc_bootstrap.c (freshness rule).

Gates: FIXPOINT OK; parity 35->36/54 (test_float_ffi); run_tests.sh 69/69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (rejection 4->8/9)

New ep_check.ep runs between parse and codegen (imported by epc.ep). Mirrors the
Rust type checker's hard-error classes it can decide structurally:
- reserved-keyword shadowing: 'channel' as a variable or parameter name
- Send safety (E0036): a borrowed reference sent to spawn/send
- heterogeneous list literals: string mixed with non-string elements

Walks functions, methods, and nested control flow. Prints 'Type Error: ...'
diagnostics and aborts compilation on any finding; valid programs unaffected.

Remaining wrongly-accepted: test_enum_type (needs full enum-payload return-type
inference — deferred to the deeper type-inference work).

Gates: FIXPOINT OK; parity 36/54 held; rejection 4->8 of 9; run_tests.sh 69/69.
Bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs lexing/parsing/semantic checks with no codegen and reports 'Check passed'
or the type errors, reusing ep_check.check_program. First self-hosted developer
tool beyond the compiler driver.

Gates: FIXPOINT OK; parity 36/54; run_tests.sh 69/69; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…str (Phase 7)

ep_optimizer.ep folds literal-OP-literal integer arithmetic at the AST level
(imported by epc.ep, runs after the checker). Identity eliminations like
x + 0 -> x are deliberately omitted: the self-hosted compiler's <heap> + 0
pointer-coercion idiom must survive (a heap handle is never an integer literal,
so literal-only folding can't touch it — verified: xs + 0 still coerces).

Folding 0 - 1 -> -1 exposed a latent bug: cg_int_to_str's 'while temp > 0' loop
never ran for negatives and returned '' (emitting a bare 'LL'). Fixed to emit a
leading '-' over the magnitude.

Gates: FIXPOINT OK; parity 36/54 with outputs unchanged (behavior-preserving);
run_tests.sh 69/69; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
run_tests.sh compares via $(...) capture, which strips trailing newlines on both
sides; the parity harness diffed against the raw file, so .expected files
lacking a final newline (test_enum_method, test_stdlib_import, etc.) counted as
mismatches even though the programs' output is correct. Match run_tests.sh
exactly. No compiler change; these tests were already passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove stray epc_stage1 binary; ignore tests/**/*.dSYM and /epc_stage1.
- README: self-hosted parity 40/54, full lex->parse->check->optimize->codegen
  pipeline, and the honest remaining gap (trait vtables, struct float fields,
  enum return-type inference; Rust retains LSP + transpilers).

Final matrix: cargo 0 warnings; run_tests.sh 69/69; fixpoint byte-identical;
epc parity 40/54 (8/9 rejections); bootstrap/verify.sh fully self-contained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Top-level 'set NAME to expr' statements are now parsed as global constants
(program AST index 9), threaded through parse_all_modules, and emitted by
codegen as file-scope globals with GC mark functions (__ep_mark_globals_*) and
an __ep_init_constants() run from main before user code — mirroring the Rust
compiler. Enables mutable global collections (the bridge-library pattern).

Gates: FIXPOINT OK; parity 40->41/54 (test_global_mutation); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A user function named after a C keyword (e.g. 'double') emitted invalid C
('long long double(...)'). New cg_sanitize_name prefixes C keywords / libc
clashes with ep_ (mirrors the Rust sanitize_c_name), applied at definitions,
prototypes, call sites, and function-pointer references.

The membership test contains_string_val hit the same unknown-typed-'equals'
->pointer-compare bug as before; coerce the left operand via string_concat so
it strcmp's.

Gates: FIXPOINT OK; parity 41->42/54 (test_hof_named); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'check' assumed every arm was an enum variant and emitted _match_tag ==
EP_TAG_<name>, so matching on strings/ints (if "red": / if 200:) produced
undeclared EP_TAG_red / EP_TAG_200. The parser now records each arm's pattern
kind (string 26 / int 25 / variant), and codegen emits strcmp for strings,
== for ints, and the tag comparison only for real variants.

Gates: FIXPOINT OK; parity 42->44/54 (test_types, test_all_phases);
run_tests.sh 69/69; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iver list)

resolve_import_path only knew 11 stdlib modules; add sort/datetime/os/test/log/
sync/regex/csv/websocket/static_server/toml/select/structured so 'import
"static_server"' etc. resolve to stdlib/<name>.ep instead of failing relative
to the source dir.

Gates: FIXPOINT OK; parity 44/54; run_tests.sh 69/69; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…5/54)

Add the remaining 'is ...' comparison phrases (is more/fewer/smaller/bigger/
larger than, is at least/most, is different from, is the same as) plus
'does not equal', mirroring src/lexer.rs. New lex_is_phrase2 helper.

Gates: FIXPOINT OK; parity 44->45/54 (test_english_aliases); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Trait impls (program index 8) were parsed but never codegen'd, so p.size()
hit 'call to undeclared function size'. Emit each impl's methods as functions
with self prepended (mirroring regular method_defs), dispatched by bare name.
Deduplicated across impls so a method name shared by multiple types (e.g.
to_string for Point and Shape) is emitted once rather than colliding.

Gates: FIXPOINT OK; parity 45->46/54 (test_traits); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closures reused spawn_index for their _ep_closure_N names, so a closure defined
before a spawn desynced the spawn arg-struct index (spawn_args_1 referenced but
spawn_args_0 defined). Give closures a dedicated counter (state slot 13).

Gates: FIXPOINT OK; parity 46->47/54 (test_closure_spawn); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MettaMazza and others added 21 commits July 2, 2026 13:06
… 47->48/54)

- Parser captures function return-type annotations (func node index 5).
- codegen builds function->return-enum-Ok-variant and variant->field-type maps.
- 'try f(...)' where f returns an enum now unwraps: non-Ok tag propagates
  (ret_val = enum; goto L_cleanup), else yields the Ok payload — mirroring the
  Rust compiler's first-variant-is-Ok convention.
- 'check' arm bindings are now typed from the variant's declared field types,
  so an Error(message as Str) payload displays as %s, not the raw pointer.

Gates: FIXPOINT OK; parity 47->48/54 (test_errors); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 48->49/54)

Cleanup freed EVERY local list at scope exit without escape analysis, so a
function that builds a list and returns it inside a constructed value —
e.g. json_parse_object's create_json_object(keys, values) — had keys/values
free_list'd at L_cleanup, freeing data the returned node still referenced. The
caller then saw empty/garbage collections (json_parse returned an empty object).

Add var_returned_in_stmts: skip the cleanup free for any list that appears in a
return expression (directly or as a call/constructor argument). The GC reclaims
it. Matches the Rust compiler's move-out semantics; general fix for any
build-and-return-a-collection function.

Gates: FIXPOINT OK; parity 48->49/54 (test_json_limits); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The move check marked the RHS of 'set x to y' as moved, so a later use of y
(e.g. 'set rel_path to safe_path' then 'string_length(safe_path)') was rejected
as use-after-move. In a GC-managed language both names safely reference the same
object, and the reference compiler permits it. Drop the move-mark for direct
assignment; the move-while-borrowed check and cross-thread send/spawn ownership
transfer are unchanged, so the rejection suite still enforces 8/9.

Gates: FIXPOINT OK; parity 49->51/54 (test_static_path_traversal, test_sql_params);
run_tests.sh 69/69; rejection 8/9; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Corrects the number: the move-aliasing fix reliably unblocks
test_static_path_traversal (49->50). test_sql_params extracts a query column
as 0 (reads uninitialized memory in the self-hosted-compiled sqlite wrapper),
so it is not a stable pass; counted as failing.
…rs (parity 50->51/54)

'for each x in iter' where iter implements the Iterator trait now uses the
iterator protocol instead of the list protocol (which segfaulted treating the
struct as a list). generate_c records struct types implementing Iterator (state
slot 18); collect_var_types tags 'set v to create <IteratorStruct>' vars with a
dedicated iterator type-code (9); for-each on a type-9 value emits a loop that
calls next() until the enum's Done variant, binding the Next payload each pass.

Gates: FIXPOINT OK; parity 50->51/54 (test_custom_iterator); run_tests.sh 69/69;
bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scape UAF)

The earlier return-escape check only caught lists directly in a return
expression. A list can also escape transitively — each 'row' appended into the
returned 'rows' (sql_query_params), etc. Freeing those at cleanup corrupted the
returned value. Since the generational GC reclaims unreachable lists after the
roots are popped, drop the cleanup free_list entirely: correct, and simpler than
a full transitive escape analysis. (sql_query_params now returns intact rows;
display of the string column remains a separate Any-typing gap.)

Gates: FIXPOINT OK; parity 51/54; run_tests.sh 69/69; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esults (parity ~51-52/54)

display of a direct get_list/pop_list/map_get_val/map_get_str call now routes
through ep_auto_to_string, so a string element (e.g. a SQL column) prints as
text rather than as its %lld pointer. Narrow/syntactic — ordinary int displays
are unaffected (verified test_errors and the suite unchanged).

Combined with the GC-based cleanup (no auto free_list), test_free_reassign
(double-free) and test_json_limits (escape UAF) pass, and test_sql_params now
prints correct rows (occasionally flaky via the ep_auto_to_string pointer probe
on the non-GC-registered strdup'd column text).

Gates: FIXPOINT OK; parity 51 stable (52 when sql_params's probe succeeds);
run_tests.sh 69/69; no epc compile flakiness; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ep_sqlite3_column_text strdup'd the column but left it untracked, so it leaked
and ep_auto_to_string had to guess via a memory probe (flaky). Register the copy
with the GC (EP_OBJ_STRING): reclaimed properly, and detected deterministically
as a string. Shared runtime — ep_runtime_gen.ep regenerated; Rust emission
picks it up via include_str!.

Gates: FIXPOINT OK; epc parity 52/54 STABLE across 5 runs (was flaky 51-52);
run_tests.sh 69/69; bootstrap regenerated. Remaining: test_async_loop (valid
output, nondeterministic worker order) and test_task_group (async scheduler).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oup works)

The self-hosted compiler emits THREAD-based async (a detached pthread runs the
body and sets fut->completed), but wait_task_group only drove the coroutine
run-queue — so with an empty run-queue it declared deadlock while worker threads
were still finishing. Poll (bounded, 1ms) for future completion instead of
exiting. The Rust compiler's coroutine async keeps the run-queue/timers busy so
it never reaches this branch; behavior there is unchanged.

Gates: FIXPOINT OK; test_task_group now runs to completion (exit 0);
run_tests.sh 69/69; bootstrap regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g (async_loop passes)

Replaces the self-hosted thread-based async with the Rust compiler's coroutine
state-machine model, reusing the shared runtime's scheduler:
- async fn -> args struct (state + future + persisted params/locals + awaited
  futures), a _step function (switch on state), and a public wrapper that
  enqueues a task and returns the future.
- await inside async -> yields (saves state, registers waiting_task, returns
  -999999) via emit_async_yields, resuming at the case label; value read from
  the persisted awaited_fut slot. await outside async -> ep_await_future drives
  the event loop.
- In a step body, params/locals are rewritten to args->name and return is
  direct. count_awaits/emit_async_yields added; state slots 19-21.

sleep_ms already returns a timer-future, so  yields and the
single-threaded event loop interleaves workers deterministically — test_async_loop
now matches the golden ordering. test_task_group still passes on the coroutine
scheduler.

Gates: FIXPOINT OK; async_loop + task_group pass; run_tests.sh 69/69; bootstrap
regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
epc now compiles and passes every runnable test the Rust compiler does (54/54,
stable across 8 harness runs), enforces 8/9 compile-error rejections, reaches a
byte-identical fixpoint, and builds with no Rust via the frozen bootstrap.
…ee (54/54 + 9/9, zero caveats)

The self-hosted semantic checker gained enum-variant field-type checking so
test_enum_type is correctly rejected (rejections 8/9 -> 9/9). That exposed two
distinct heap-use-after-frees that only surfaced on enum-heavy programs
(test_errors, test_types, test_enum_method):

1. ep_check.ep bound a shared sub-list to a local: `en_field_types` returned
   `get_list(vf, i)` (a list aliased inside en_vf), which the Rust codegen's
   cleanup pass then free_list'd at function exit, corrupting en_vf. Replaced it
   with `en_field_type_at`, which reads the shared vf sub-lists strictly inline
   and returns a freshly-built string — no list-typed local is ever aliased.

2. ep_codegen.ep's list-reassignment path emitted `free_list(old)` before the
   new value. For a per-iteration `fts` that had already been appended into
   en_vf, that freed a value the container still owned -> use-after-free on the
   next iteration. Removed the auto-free (mirroring the earlier cleanup-free
   removal); the generational GC reclaims genuinely-unreachable lists.

Verified ASAN-clean under BOTH the Rust-built and the clang-only bootstrapped
epc. Gates: parity 54/54 runnable, 9 rejected / 0 wrongly accepted, 3-stage
fixpoint byte-identical, bootstrap/verify.sh (Rust-free) green, run_tests.sh
69/69, zero cargo warnings. Frozen bootstrap regenerated in the same commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t_readable builtin

- emit_js.rs: enums now transpile to ES classes carrying a variant tag and an
  ordered payload-field list, and `implement Trait for Type` methods are emitted
  as prototype methods, so `value.method(...)` dispatches to the right impl and
  positional pattern-binding works in the JS backend.
- async_wait_readable(fd) -> Future<Int>: registered in the type checker and C
  codegen and implemented in the runtime; suspends an async task until the fd is
  readable so I/O-bound tasks yield the event-loop thread instead of blocking.

run_tests.sh 69/69, zero cargo warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hosting story

- README: lead with the self-hosting/bootstrap achievement; real per-module line
  counts (reference ~30,094 lines / 24 modules; self-hosted ~6,400 lines / 6
  modules; shared runtime 4,893 lines); a "what done means" gate table
  (69/69 Rust, 54/54 runnable, 9/9 rejections, byte-identical fixpoint,
  clang-only bootstrap); a Test Matrix section; badges updated to the real
  numbers.
- AGENT.md: list all six self-hosted modules (ep_check + ep_optimizer were
  missing), correct the line count (5,800+ -> ~6,400), add the current gate
  results and the clang-only verify.sh gate.

No code change; documentation only. All gates remain green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… type-safety holes

Adversarial differential testing (37 novel programs compiled by BOTH compilers,
outputs diffed) surfaced and fixed:

1. MISCOMPILATION (self-hosted): `not (a > b)` emitted C `!a > b` — C precedence
   negated only `a`, silently flipping branches. Unary-not now parenthesizes its
   operand (ep_codegen.ep).

2. GRAMMAR DRIFT: the README-documented single-line closure `given x: return
   x * 2` parsed under epc but not under the Rust compiler. The Rust parser now
   accepts it (src/parser.rs).

3. CLOSURE NAME COLLISIONS (Rust): two functions using the same closure variable
   name, or one closure duplicated by loop unrolling, emitted colliding
   `_ep_closure_<var>` C functions (clang redefinition error). Lifted closures
   now get globally unique names (src/codegen.rs).

4. TYPE-SAFETY HOLES (both compilers), all now hard errors in BOTH:
   - `returning Int` + `return "str"`: the README's own flagship rejection
     example was actually accepted (downgraded to a warning long ago). Literal
     return-type conflicts are hard errors again; the `return 0` null idiom and
     inference-var cases stay warnings. tests/test_return_type_mismatch.ep is
     now an expected_compile_error test (it previously codified the warning).
   - `"abc" + 5`: C pointer arithmetic in disguise (printed adjacent literal
     pool memory). Str operands in arithmetic now allowed ONLY as the `+ 0`
     int-cast idiom.
   - `double_it("five")` with `n as Int`: epc's checker now verifies call-site
     argument types against declared param types. The self-hosted parser now
     captures `as Type` annotations (param node index 2) instead of discarding
     them.

New rejection tests: test_str_arithmetic, test_arg_type_mismatch.

Gates: Rust suite 71/71; parity 53/53 runnable + 12/12 rejected (0 wrongly
accepted); 3-stage fixpoint byte-identical; clang-only bootstrap verify green
(re-frozen in this commit); ASAN clean on the checker paths; 37/37 adversarial
programs agree across compilers; zero cargo warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er's prose

The fuzzy did-you-mean matcher ran against the error message's own words, so
nearly every parse error suggested "Did you mean 'for'?" (Levenshtein-matching
the word 'found'). It now matches only user-quoted text, and the most common
mistakes get targeted plain-English hints:
- commas between arguments -> shows the 'and' form: add(1 and 2)
- missing ':' on a block line -> says which lines need ':'
- inconsistent indentation -> says blocks must align
- unknown statement start -> lists the statement keywords

Rust suite 71/71.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…declared params

The GC-root analysis already skipped rooting for Int/Float/Bool locals used
only in primitive expressions, but treated ANY user-function call as
non-primitive usage — so fib(n - 1) forced n to be rooted and the function to
run push_root/maybe_collect/pop_roots on every call: measured 6.3x slower than
C on fib(35). Passing a variable to a parameter DECLARED Int/Float/Bool is now
primitive usage (new prim_param_flags map, threaded through the analysis).

fib(35): 0.19s -> 0.03s user — identical to clang -O2 C.

Safety: rooting only skipped when the callee's parameter is explicitly
declared primitive; unannotated params keep conservative rooting. Verified:
Rust suite 71/71, parity 53/53 + 12/12 rejections, byte-identical fixpoint,
clang-only bootstrap verify, GC stress tests (remembered-set UAF, oldgen)
ASAN-clean, zero warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, docs truth-up

- tests/run_differential.sh + tests/differential/: 37 adversarial programs
  (operator precedence, GC/closure corner cases, unicode/string edges,
  rejection agreement) compiled by BOTH compilers, outputs diffed. This is the
  suite that caught the `not` miscompilation, the closure grammar drift, and
  the three type-safety holes. 37/37 agree.
- CI now runs the full gate matrix on macOS + Linux: run_tests.sh, parity,
  differential, 3-stage fixpoint, and the clang-only bootstrap verify —
  previously it only built and smoke-tested a handful of files.
- README/AGENT.md: current numbers (71/71, 53/53 + 12/12, 37/37), the
  differential gate documented, and an honest measured benchmark: fib(35)
  Ernos 0.03s vs C 0.03s user time (after the GC-rooting optimization).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tring fix

LICENSE: MIT — free for anyone, for any purpose.

examples/bakery.ep: a ~300-line day-in-the-life simulation exercising the
whole language in plain English — structs + methods, traits, enums + check,
try/Result, closures (single-line and block), a custom Iterator, async ovens
on the coroutine event loop, spawned threads + channels, floats, f-strings,
maps, lists, plain-English insertion sort, break/continue, recursion at C
speed, file I/O, and SHA-256 receipt stamps. Both compilers produce
byte-identical output for it; CI now proves that on every push.

Writing the flagship surfaced a real bug, fixed in BOTH compilers: an
f-string interpolating a Float printed the double's raw bit pattern as a huge
integer (ep_auto_to_string cannot know the bits are a double). New shared-
runtime ep_float_to_string (%.15g, matching display) and both codegens route
Float-typed interpolations to it. Regression test added to the differential
suite (now 38 programs).

Gates: Rust suite 71/71, parity 53/53 + 12/12 rejections, differential 38/38,
fixpoint byte-identical, clang-only bootstrap verify green (re-frozen here),
zero warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MettaMazza MettaMazza merged commit 9c07d3b into main Jul 4, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant