Files
el/lang/AGENTS.md
bigmerge 45325f7391
El SDK CI - dev / build-and-test (pull_request) Failing after 4m6s
singleton: guard the state, not the program's name
The singleton lock protected a filename, not a store. It was keyed on
$EL_SINGLETON_DIR|$TMPDIR|/tmp + /el-singleton-<program>.lock — the
program's NAME and a temp directory — and never consulted the state it
claimed to protect, while its own refusal message read "Refusing to start
a second instance against the same state."

Measured, it failed in both directions. A second engram against a
DIFFERENT data dir was refused, naming the first's pid. And
TMPDIR=/tmp/other let a second engram start against the SAME data dir
with no complaint — the two-writer data-loss condition the guard exists
to prevent, defeated by one environment variable.

Both are one error: the identity of the resource had been replaced by a
label for it.

The lock now lives inside the state it guards —
<state>/.el-singleton-<id>.lock — and the program block says what that
state is. Same directory is the same file is the same inode, so it
contends and there is no TMPDIR left in the key to change. Different
directories are different files, so they don't. Different spellings of
one directory (trailing slash, x/../x, symlink) collapse in the kernel's
own path walk, so they contend without this code comparing strings;
canonicalisation is for the message, never the decision.

`guards:` is an expression so a program can point at the resolver that
already owns its path — guards: engram_resolve_data_dir() — instead of
restating that resolver's default, which is the two-owners defect spec
18.4 exists to prevent. A `singleton:` without `guards:` is now a compile
error; emitting a name-keyed lock instead would be emitting the defect.

Kept: the flock (the kernel drops it on crash and SIGKILL, so there is
still no "delete the lock file to get unstuck" ritual — a stale file
inside a copied data dir is inert), and the holder's pid in the message.
Changed: the message is true. It says "the same state" because the lock
it failed to take is in that state, and it names the state it checked.
An unguardable state (missing, read-only) now refuses rather than
starting unguarded.

Also corrects lang/AGENTS.md's compiler rebuild line, which had gone
stale: linking el_runtime.c alone no longer resolves.
2026-08-16 16:08:40 -05:00

12 KiB

El Language — Agent Guide

El is a self-hosting, statically-typed language that compiles to C. This file orients agents that work on El itself or on programs written in El.


Current work in this worktree — the API reshape / decorated seam (IN PROGRESS, 2026-08-14)

This is the api-reshape worktree. The build here reshapes Neuron's external surface and how it is declared — proven on isolated dev-port clones only; live prod engram :8742 is untouched and nothing is promoted. Full framing lives in neuron/docs/architecture/06-cognitive-architecture.md (Update — 2026-08-14 deep night) and 02-components.md §5.

  • Surface collapse. The ~90 noun-organized CRUD MCP tools collapse to a few geometry opsread (the vantage-read: re-origin + salience/recency + an aperture → a bounded slice, curing the whole-self dump), write, relate, supersede (evolve/tombstone/promote, never a hard delete) — plus the agentic primitives think/attend/learn/ground/assert. The old noun is a type parameter. Implemented in tools/api-reshape/surface.el with a parity harness (parity.sh); aperture proven to bound output. Not yet: compiled into the MCP servershipped (verified 2026-08-16): the live MCP surface is exactly these nine ops (read · write · relate · supersede · think · attend · assert · ground · learn); the ~87-tool surface is gone. attend absorbed getInstructions / beginSession's active-context sweep / checkEvents — those are gone, not gapped. Still outstanding: hot-swap, all-alias dispatch.

    ⚠ Two of those primitives are the wrong shape, and it is documented (2026-08-16). think({seeds, faculty}) treats faculties as parameters; they are operationsreason changes the estimate (a read), induce changes the parameters, abduce changes the structure (a write GeoGradient cannot express). And ground mints a grounded-by edge, but grounding is not a subsystem — it IS the edge weight: a property of a relation, not a relation between nodes. Authority: lang/spec/correspondence-and-censorship.md. Do not re-derive it; if you think a section is wrong, say so with a measurement.

  • Decorated seam. @route(path,method,…) makes codegen synthesize el_route_dispatch (replacing the hand-written handle_request if-else) — proven decorate→serve on :8951. @manager/@engine/@accessor are parsed but structurally inert in the shipped compiler today; the @route codegen lives on the unmerged branch feat/el-route-decorators. Telemetry-emit and dharma-bus auto-wiring at the boundary are staged, not shipped. In-process, an @accessor reaches the engram via engram_* builtins, not http_get.

Do not edit the protected build sources while this is in flight: el-compiler/src/codegen.el, el-compiler/runtime/el_seed.c (and the archived legacy/el_runtime.c), the runtime/engram_*.c boot files, and surface.el (when present in the reshape tree) — these are owned by the build agents.


What El Is

El compiles .el source → C → native binary. Every El value is el_val_t (int64_t). Strings are heap pointers cast through int64_t. The compiler is written in El (self-hosting).

The compiler pipeline:

elc-cli.el
  └─ imports: compiler.el
       └─ imports: lexer.el, parser.el, codegen.el, codegen-js.el

The canonical compiler binary is dist/platform/elc. It was produced by running an earlier version of itself on elc-cli.el.


The Two Layers — Know Which One You're In

Layer 1: El programs (.el files)

This is where almost all work belongs. El programs are source files that get compiled by elc. New library functions, application logic, and language-level utilities all go here as .el files.

Do not add C code when El can express it. If functionality can be built from existing El primitives (string ops, exec, fs_read/write, http_post, etc.), write it in El.

Layer 2: The C seed (runtime/el_seed.c)

This is the self-contained C OS-boundary layer. It provides the __-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is not generated — it is maintained by hand.

The runtime is native El (runtime/*.el) over a C OS-boundary. Status (verified 2026-08-15): the migration to a seed-only boundary is in progress, not done. Two files exist:

  • runtime/el_runtime.c (~860 KB) — LIVE. Holds the engram store (EngramStore engram_global) plus the http_*/json_*/state_*/engram_* impls. It is the authoritative single-file link target for the compiler, and tools/install.sh compiles it into libel.a. This is where a new C builtin's implementation must currently live to be linkable.
  • runtime/el_seed.c — the intended hand-maintained __-prefixed seed (thin wrappers over the above). It is compiled alongside el_runtime.c by tools/install.sh, but does not compile standalone yet (see the build-path caveat under "Rebuilding the Compiler").

Only edit these when you genuinely need OS-level access (raw sockets, GPU calls, new libcurl features, a new engram store op). For everything else, write El.

When you add a C builtin (verbatim-emit recipe — the El name is emitted as the exact C symbol; builtin_arity is an arity guard only, not a dispatch table):

  1. Implement the C function in el_runtime.c (and declare it in el_runtime.h).
  2. Add a __-prefixed thin wrapper in el_seed.c and declare it in el_seed.h.
  3. Add the name to builtin_arity in el-compiler/src/codegen.el — add both the plain and __-prefixed spellings.
  4. Rebuild the elc binary (see below) and confirm the self-host fixpoint is byte-identical.
  5. Prove it with a NEGATIVE CONTROL. Show the test FAILING on a build without your change, then passing with it. A test that has never been seen to fail has proven nothing.

Step 5 is not optional, and step 4 does not cover it. The fixpoint proves the compiler reproduces itself. It says nothing whatsoever about whether your builtin works. A recipe ending at "byte-identical" reads as complete while having verified nothing about the thing just added — which is why this file, until 2026-08-16, produced builtins with no tests at all.

Measured cost of the omission (2026-08-16): engram_node_set_emb, engram_curiosity_json and dream_set_handler were all added in one session with zero tests. Separately, a UTF-8 fix was written, tested, and the test passed on the unpatched build too — the defect was elsewhere entirely, and only building the pre-fix binary exposed it. Without a negative control that fix would have merged as verified.

Two shapes that pass while proving nothing, both hit the same day:

  • A test that never exercises your change (the route supplied a default that bypassed the code under test).
  • An induction that loses a race. curl --max-time on a large response left both builds alive; only SO_LINGER 0 — a genuine RST, so the peer is provably gone — reproduced the failure. Six of ten attempts is not a control.

Before every probe, confirm your process bound the port (lsof -nP -iTCP:<port>, match the PID). A stale instance answering on the port has silently produced false results here more than once, and pkill -f does not reliably match an argv like ./engram.

Worked example: the engram_assert_json (op_assert seam) and engram_node_full_in/engram_connect_in (purview write-side) primitives added 2026-08-15 follow exactly this recipe.


Rebuilding the Compiler

After changing any .el source in el-compiler/src/ (run from the lang/ dir):

# 1. Stage2: current elc compiles the (modified) compiler to C
./dist/platform/elc elc-cli.el > elc-new.c
# 2. Build the new compiler. Link the WHOLE runtime set, not el_runtime.c alone:
#    el_runtime.c calls into engram_store / engram_vindex / eg_cosine_batch and
#    wraps el_seed.c, so a one-file link fails at `ld` with undefined symbols
#    (verified 2026-08-16 — the previous single-file line in this doc is stale).
cc -std=c11 -O2 -I runtime -I$(brew --prefix openssl@3)/include \
   -L$(brew --prefix openssl@3)/lib \
   -o dist/platform/elc-new \
   elc-new.c runtime/el_runtime.c runtime/el_seed.c \
   runtime/engram_cognition.c runtime/engram_geometry.c runtime/engram_reason.c \
   runtime/engram_store.c runtime/engram_verify.c runtime/engram_vindex.c \
   runtime/eg_cosine_batch.c runtime/eg_cosine_batch_strategy_cpu.c \
   -lcurl -lssl -lcrypto -lpthread -lm
# 3. Verify self-hosting FIXPOINT (stage3 == stage2 output, byte-identical):
./dist/platform/elc-new elc-cli.el > elc-verify.c
diff elc-new.c elc-verify.c   # must be identical
mv dist/platform/elc-new dist/platform/elc

Build-path caveat (verified 2026-08-15). el_seed.c is the intended hand-maintained OS-boundary seed, but it does not compile standalone under modern clang: it wraps ~16 unprefixed el_runtime.c symbols (http_serve, json_*, state_*, http_response) without prototypes, and clang treats implicit declarations as errors (C99+). The productionised install (tools/install.sh) builds libel.a from both el_seed.o + el_runtime.o together, which is why linking succeeds there. To make el_seed.c build on its own, add prototypes for those symbols (or #include "el_runtime.h", reconciling the __http_serve return-type mismatch first). Until then, el_runtime.c is the authoritative single-file link target for the compiler.

After changing el_seed.c only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.


How El Programs Are Built

Each El application has a build.sh that:

  1. Concatenates all .el source files (stripping import lines)
  2. Runs elc to produce a .c file
  3. Runs cc linking against el_seed.c

Example (cgi-studio daemon):

cd products/cgi-studio/el-daemon
./build.sh

When you add a new .el file to an application, add it to that application's build.sh concat list.


Parallelism in El

El is single-threaded at the application level. Parallelism is achieved through subprocess fan-out:

// Pattern: write payloads to temp files, exec bash script with & and wait,
// read results back from temp files.
fn http_post_parallel(urls: [String], bodies: [String]) -> [String] {
    // ... bash fan-out via exec() ...
}

Use exec() (blocking) or exec_bg() (fire-and-forget) with shell scripts to run concurrent work. There is no goroutine or async/await — parallelism goes through the OS process layer.


Key Files

Path What it is
dist/platform/elc Canonical compiler binary (arm64 Mac)
el-compiler/src/codegen.el Code generator — builtin arity table lives here
el-compiler/src/lexer.el Lexer
el-compiler/src/parser.el Parser
runtime/el_seed.c Self-contained C OS-boundary layer (replaces el_runtime.c)
runtime/el_seed.h Seed header (C function declarations)
spec/language.md Language specification
BOOTSTRAP.md How to recover the compiler from scratch
elc-cli.el Compiler entry point
elc-combined.el Pre-merged single-file compiler (used during early bootstrap)

HTTP Timeout

The El HTTP client (libcurl) defaults to 60 seconds. Override per-process via EL_HTTP_TIMEOUT_MS env var. Set it before spawning any subprocess that makes long API calls:

exec("EL_HTTP_TIMEOUT_MS=300000 " + SOME_BIN + " " + args + " 2>&1")

Rules

  • New library functions → write in El
  • New OS/hardware primitives → write in C and register in codegen.el arity table
  • Never edit dist/platform/elc directly — always rebuild from source
  • Never modify el_seed.c to add functionality that El can express