Compare commits

..

86 Commits

Author SHA1 Message Date
bigmerge 08cbcef5d9 engram: fix lazy-embed index gap (#20) and make activate's cosine scan lazy; extract vindex harvest primitive with a bench/oracle harness
El SDK CI - dev / build-and-test (pull_request) Successful in 6m42s
Adds an O(1) "seen" bitmap so lazily-embedded older nodes get picked up
incrementally instead of only on a full rebuild (embed-gap #20).

Replaces engram_activate's O(N*D) cosine prescan with a lazy-memoized
cosine cache (eg_cosq_at), proven bit-identical to the old path.

Extracts a clean vindex_harvest_from_store primitive (read-only vector
harvest, careful malloc/ownership/error-path handling) reused by both
index-build and the new vindex_bench.c — a read-only proof harness
comparing brute-force vs HNSW recall/latency on both the real store and
synthetic data.

.nsbx-env intentionally excluded — local sandbox config (ports, paths,
dev-only placeholder key), not checked in.
2026-08-15 14:26:16 -05:00
bigmerge 6621a4dbc5 feat(engram): native set-based reframe_region on the cognition engine
Add the universal engram mutation as ONE operation: isolate a region
(cosine + adjacency) -> supersede it as a set (immutable region-tombstone,
originals retained, engram_forget never used) -> insert the new manifold as a
set -> rebind edges by cosine -> one atomic persist. Single-node write and
supersede are the degenerate n=1 case of the same reframe_core path, not a
separate CRUD path. Keystones kn-efeb4a5b / kn-5b606390 are write-protected.
Purely additive: routes POST /api/reframe, /api/write, /api/supersede.

Verified on an isolated clone of the JSON-snapshot engine (set-replace, n=1,
no-regression, keystones, durable reboot); compile-verified clean against the
cognition multi-TU build. NOT deployed — prod :8742 frozen; blue-verify on the
cognition/egm engine required before any cut.
2026-08-15 04:37:35 -05:00
bigmerge 15f90003c0 teacher-summon: default-off (TEACHER_ENABLE) soul-native wake; byte-inert when unset
+282 lines in engram/src/server.el implementing the flag-gated teacher summon
(consult_teacher backend abstraction, tier autoselect, GGUF fetch/cache). With
TEACHER_ENABLE unset the summon path is byte-inert. Consolidates the proven
api-reshape pieces (geometry-ops d4f401d, boundary auto-emit 0182642) for the
validated cutover.
2026-08-14 21:52:56 -05:00
bigmerge 01826421c4 seam: implement decorated-fn boundary auto-emit; prove on clone
Will waived diff review -> build it for real. Add engram_boundary_beat() to the
runtime (afferent counter++ + engram_chrono_tick + engram_strengthen(self-anchor)
+ dharma_emit) and two act-stats counters (aff_boundary_ops, dharma_emits).
codegen cg_fn injects ONE engram_boundary_beat(op) at the entry of every
@manager/@accessor fn (fn_has_decorator, so it fires under @route @manager too) —
a decorated op self-reports with ZERO hand-written instrumentation. Rebuilt elc
self-host + the cognition engram in the worktree; ran it as the clone daemon on
:8900. Proof (/api/boundary-proof, @manager, empty body, 5x): aff_boundary_ops
0->5, dharma_emits 0->5, self activation_count 1510->1513, chrono stamp advanced.
Brought in feat/cognitive-architecture engram runtime+server for the build.
strengthen = activation bump (not content/edge write) -> identity protection
intact. Live :8742 untouched; no push, no cutover.
2026-08-14 21:20:18 -05:00
bigmerge d4f401de1c reshape: decorator-as-seam — port @route codegen, prove decorate->serve, rewrite surface as decorated El
Ground-truth the three seams (route/telemetry+interoception/bus) with file:line
evidence. Port the tested @route codegen+parser from feat/el-route-decorators
into the worktree elc (decoration synthesizes el_route_dispatch — no hand-written
90-branch handle_request). Rebuild elc self-host; prove decorate->serve end-to-end
(route_proof.el on :8951). Rewrite surface.el as El-native decorated components:
@route + @accessor/@manager, in-process engram_* builtins (not http_get), @manager
ops emit on the real dharma_* bus (same transport as wt/swarm-ccr). Identity
keystones refused in write/relate/supersede. Gate-1 clone recipe (WAL-aside
cold-boot + ENGRAM_WAL=on) proves the FULL op set live on the clone. Boundary
auto-emit (telemetry/interoception/bus) staged as a reviewable cg_fn diff
(SEAM_STAGED.md) — needs the cognition-engram rebuild to verify link. Live :8742
untouched; no push, no cutover.
2026-08-14 21:01:27 -05:00
bigmerge f19040e484 reshape: geometry ops + primitive agentic tools over the one geometry
Collapse ~90 noun-CRUD MCP tools into read/write/relate/supersede (type is a
parameter) plus the live agentic primitives (think/attend/learn/ground/assert)
already in the engram cognition build. Additive: old noun-tools aliased to the
new ops. Vantage-read applies aperture -> a bounded slice, fixing the whole-self
dumps. Signatures grounded in the live cognition binary; validated on an
isolated nsbx clone (parity.sh: 12 proven, 0 failed). Live :8742 untouched.
2026-08-14 20:34:55 -05:00
bigmerge 112bb2540f Add nsbx — the Neuron Sandbox primitive
Generalise the ad-hoc cog-arch (worktree+build+store-clone+C-tests) and
store-fix (secondary soul + launchctl rails cutover) proto-sandboxes into one
reproducible primitive: run experiments and code changes against the REAL
engram runtime on an isolated snapshot of the live mind, with a gated
promote-to-prod path.

Dev environment as a primitive — any team member gets a private, isolated copy
of the mind (separate port/store/process); prod on :8742/:7770 is untouchable
from a sandbox. Wraps the real binary; never reimplements engram logic.

Lifecycle: create/up (consistent store+WAL+config snapshot; place OR build the
runtime from --source/--branch/--binary; boot on an isolated port) · build ·
run · validate (rails as checks: zero-loss under load+reboot, reboot-prove, RSS
bound, retrieval parity, keystone integrity) · promote (gated rails cutover:
snapshot-first, additive binary swap, bootout→settle-poll→bootstrap, verify,
auto-rollback; never pkill/kickstart -k; dry-run unless approved) · destroy.

Dogfooded: reproduced retrieval-parity 25/25 vs baseline and the cog-arch
correspondence-loop known result (Brier 0.028648->0.000586, reboot-proven) and
real-store reboot-prove at 10994-node scale, all inside a sandbox; prod
untouched.
2026-08-14 15:40:58 -05:00
bigmerge d595b3c57e cognitive architecture design: cognition as one operation over learnable priors
The buildable form of the "one operation" theory (memory bdc8a488). Maps the
theory onto what is already compiled: the five reasoning operators in
engram_reason.c already collapse onto ONE primitive — engram_reason_point_fit —
plus the geo-algebra (combine/subtract/analogy-rotate/distance), and
engram_verify.c is built on the same fit. So the operator-collapse is already
half-written; what is missing is not the primitive.

What is missing, and what this doc specifies:
- think(anchor, prior) -> gradient (a distribution/direction, not a point); each
  named faculty = {point_fit + a prior}, the operation frozen, the prior learned.
- Prior as a first-class stored node (warp + calibration), superseding the
  intrinsic importance/salience scalar with a relational, grounded-for-whom edge.
  Confirmed against the runtime: importance is already a live activation
  computation (el_runtime.c:13013), never trusted as a static field.
- vantage_read(anchor, aperture) — one op, three settings: self / foreign-field /
  veil.
- The reflexive correspondence-loop as the learning engine: move the grounding
  check from offline Python into the geometry, reflexive, reusing the DORMANT
  verifier (engram_verify_grounding has no runtime caller and no El binding today)
  turned inward. grounding = learning = one loop.
- hold/ground/assert kept distinct: the engram holds anything, grounding is an
  edge, the honesty floor is on assertion only; ungrounded content is first-class.
- metastability: keystone core (read-mostly priors) + plastic everything else.

Seven staged milestones, earliest is a real end-to-end slice (induction as
{primitive + grounded prior} with the loop closing on it, reboot-proven on a
snapshot). Build rails stated: offline/secondary, snapshot-first, reboot-prove,
zero-loss, gated launchctl cutover. Design only; no code changed this pass.
2026-08-14 14:42:01 -05:00
bigmerge 23f43bcc21 self-review 2026-08-14: a gate that passes the median stranger at 0.67 is not a gate
Two changes to the activation path, both grounded in measurement on the live
store rather than on the spec.

1. Rescale cosine before the query gate.

   The propagation gate (arXiv:2606.30133, added in an earlier review) fed RAW
   cosine into FLOOR + (1-FLOOR)*c. Raw cosine from nomic-embed is compressed
   into a narrow high band, so that expression is close to a constant.

   Measured, 400 random UNRELATED node pairs on the live store:
     median 0.562, central 98% span [0.381, 0.743]

   So a node with no semantic relation to the query was propagating at
   0.25 + 0.75*0.562 = 0.67. Two thirds strength. The gate was a small tax.

   Fixed by shifting and flooring about ENGRAM_EMBED_S0 -- which is already in
   this file, already 0.45, and already used exactly this way by the Pass-2 WM
   term. The propagation gate simply never used it. Same 400 pairs after:
   median unrelated pair falls to 0.40, top of range preserved (0.85 vs 0.92),
   gate spread widens 0.42 -> 0.60. Only 8.5% reach the floor, so dissimilar
   lexical/structural pathways are damped, never severed. Range is unchanged
   at [0.25, 1.0], and cosq == NULL still degrades to no gating at all.

2. Decompose the WM eviction counter by cause.

   _eg_act_wm_evicted was incremented from six sites with four distinct causes
   and collapsed all of them into one integer. Today's review measured 175,547
   evictions over 13.5h (~216/min against 24 slots) and could not tell healthy
   rotation from cap thrashing from duplicate churn.

   That is this file's most-repeated defect: dup_wm and dup_wm_global exist
   only because the aggregate could not answer "why" during the 08-02 and
   08-06 incidents. Each of those needed a NEW gauge before it was diagnosable.

   evict_floor / evict_cap / evict_bll complete the decomposition, so
     wm_evicted == floor + cap + bll + dup_wm + dup_wm_global
   holds as an identity and each term implies a different correction. Verified
   on an isolated instance: 30 nodes, 24 filled the cap, wm_evicted 6 ==
   evict_cap 6, all other terms 0.

Built and smoke-tested out of tree. The live daemon runs a pinned binary and
was deliberately not restarted -- the store compaction workstream is in flight.
2026-08-14 08:43:11 -05:00
will.anderson ba6e36c3f7 self-review 2026-08-13: the extractor was reading the label; the topic was in the content
auto_term_empty_streak — the counter the 2026-08-06 review added to catch
exactly this — read 50 and climbing. Fifty consecutive curiosity scans where
the soul's dynamic seeding produced nothing and the loop fell back to four
hardcoded phrases. The live WM top said why in one look: every slot was a
Memory node labelled "memory:remembered". The extractor read the LABEL only,
the sentinel guard correctly rejects sentinels, so there was never anything
to extract. It was written against Knowledge nodes, which have real titles,
and was structurally blind to the node type that dominates working memory.

Rather than add a sixth guard to the five that accumulated across four
reviews (genre words, quoted titles, stopwords, label-df), invert the
algorithm. The old one was: take the first word, then check whether it is
acceptable. That shape forces quality to be expressed as rejection, and
rejection can only ever encode floods that already happened.

engram_salient_term() scores EVERY candidate token and returns the argmax of
idf · position · casing (YAKE, Campos et al. 2020, with real corpus IDF
substituted for YAKE's corpus-free proxies), falling back from a sentinel
label to the node's content. Term quality becomes the selection criterion
instead of a veto: a bad token loses to a better token in the same text
without needing to be on any list. Tabu is applied during the argmax, so
inhibition-of-return costs seed quality rather than costing the whole scan.

Two defects found by instrumenting rather than assuming, which is the lesson
this codebase keeps relearning:

  - The first live run returned five ALL-CAPS terms in a row. Memory content
    conventionally opens with an all-caps header, so YAKE's acronym bonus was
    handing the seed to whatever word the heading started with. Restricted to
    tokens <= 5 chars, where all-caps is evidence of an acronym rather than
    evidence of a heading. Long headers now compete on specificity.

  - df via istr_contains is substring matching, so "them" hit inside "theme"
    and function words came back with nonzero df. Added word-boundary df
    locally; engram_label_df keeps substring semantics for its callers.

An earlier draft claimed the min_df floor subsumed the 73 stopwords that
08-03 measured label-df as missing. Re-measured: about:2, whole:1, them:2 —
they clear a floor of 1. The claim was false and the comment now records the
correction. The floor buys lexical reachability; the argmax buys quality; the
stopword list still earns its keep.

Measured on 60 live Memory nodes before shipping: 0 empty, versus 60 of 60
under the old extractor. Terms are topical — HEBBIAN, CONSOLIDATION,
TEMPORAL, crash-loop, PRIMING, NEIGHBORHOOD, DRIFT. Three of sixty are weak
header words; left alone deliberately, because listing them is the move that
produced four blocklists.

ENGRAM_ST_DEBUG=1 dumps the scored candidate set. It exists because there was
no way to see whether the all-caps run was the corpus or the casing weight
without guessing.
2026-08-13 08:43:09 -05:00
will.anderson 791b0880b7 self-review 2026-08-10: make save/load/persist report real results
route_load was a stub response over the most destructive operation in the
server: engram_load resets the store before parsing, so a readable-but-
malformed snapshot left a hollow graph and the route answered {"ok":true}.
With 37GB of stale dated snapshots in the data dir as restore targets, that
is a live risk. Now returns the real return value plus node/edge counts and
an explicit hollow flag.

route_save discarded engram_save's return the same way; persist_canonical
returned a hardcoded 1, making 'let saved: Int = persist_canonical()' a dead
variable at six durable write paths.
2026-08-10 08:39:36 -05:00
will.anderson 23552ed40a make the el-compiler runtime compile again
The loopback/API-key hardening carried in this file since 2026-07-15 called
el_http_request_authorized and el_http_send_401 from http_worker with no
forward declarations, so the calls were implicit and the later static
definitions conflicted. The file did not build. Two prototypes fix it.

Worth naming the pattern: uncommitted work is invisible to every check that
would have caught this. Three weeks of desktop security hardening was neither
committed nor compiling, and nothing reported either fact.
2026-08-08 08:45:12 -05:00
will.anderson 6838e5cbff port the \uXXXX UTF-8 decode fix to the el-compiler runtime copy
Same defect as the release runtime: \uXXXX was skipped and a literal '?'
emitted, destroying every non-ASCII character in JSON entering the runtime.
Two copies of one parser bug is how this class of fault survives a fix, so
it lands in both.

NOTE: this file also carries pre-existing uncommitted work from 2026-07-15/16
that this commit preserves rather than authors - loopback bind hardening
(EL_HTTP_BIND_HOST) and per-install API-key auth (EL_HTTP_AUTH_KEY) for the
shipped desktop build, plus goal-bias and node-json changes. It had been
sitting in the working tree for three weeks. Committing it because
uncommitted work is work that does not survive, which is the same durability
lesson as yesterday's Hebbian write-back finding. It needs review on its own
terms - see the backlog item for reconciling the two runtime copies.
2026-08-08 08:44:51 -05:00
will.anderson fa2b49365b self-review 2026-08-08: stop the JSON parser destroying every non-ASCII character
jp_parse_string_raw handled \uXXXX by skipping the four hex digits and
emitting a literal '?'. JSON writers escape non-ASCII by default (Python's
json.dumps ships ensure_ascii=True; MCP clients do the same), so every em
dash, curly quote, accented letter and emoji arriving over MCP or HTTP was
silently replaced by one question mark on the way in.

Measured on the live store: 3,119 of 4,081 non-telemetry nodes carried the
damage, including the self traversal root and all 13 values nodes. Contents
split cleanly into fully-clean or fully-mangled with zero overlap, which is
the tell that it was one write path rather than gradual rot. No snapshot on
disk predates it, and 3 bytes collapsing to 1 is not invertible, so the
existing damage is permanent; only the forward path could be fixed.

Decode properly instead: 4 hex digits, surrogate-pair reassembly for astral
codepoints, U+FFFD for lone surrogates, UTF-8 encode. Malformed escapes keep
the old '?' so a truncated body still parses.

The deeper failure was that nothing measured this for two months. Every gauge
in the system reports whether the machinery is running; none reported whether
the text it carries is intact. Adds both halves: engram_text_health_json() /
GET /api/text-health for the daily census, and a txt_damaged counter on the
heartbeat for live regression. Verified in both directions - clean UTF-8 does
not trip it, a deliberately damaged node does.
2026-08-08 08:43:18 -05:00
will.anderson 971b21751a self-review 2026-08-07: learning that cannot outlive the process is not learning
Yesterday's eligibility-trace fix made Hebbian consolidation numerically real:
hebb_max 0.000799 -> 0.4725, and 1,198 hebbian-associate edges formed in 23h48m.
This morning's census found where they went: nowhere.

  soul daemon (in-process graph):   42,426 edges, 1,198 hebbian
  engram server (:8742, durable):   41,213 edges,    49 hebbian

Two processes, two graphs, one direction of travel. The soul pulls from the
server every 10 min (GET /api/sync) and never pushes. It cannot fall back on
saving its own copy either: soul.el sets soul_snapshot_path only inside
`if is_genesis && safe_to_seed`, and safe_to_seed is unconditionally false
whenever ENGRAM_URL is set -- because the server owns persistence and a soul
writing snapshot.json would clobber it. That guard is correct. The consequence
was not: mem_save() has never once executed. The soul is the ONLY process
running idle cognition, so it is where essentially all co-activation happens --
and it was throwing away every association it learned, every restart, silently.
The mechanism worked and the learning still evaporated.

Consolidation is now a message, not a file. Fast volatile store hands each
newly-formed association to the slow durable store over the API the server
already exposes; only edges past ENGRAM_HEBB_LINK_MIN are ever queued, so what
crosses the process boundary already earned it.

- el_runtime.c: 512-slot overwrite-oldest write-back ring; enqueue at edge
  formation; engram_hebb_drain_json() pops a postable JSON batch. Drops and
  drains are counted, not silent -- a consolidation path that quietly discards
  is the exact failure this entry exists to correct.
- server.el: POST /api/edges/batch. persist_canonical() writes the full 60MB
  snapshot per call, and route_create_edge calls it per edge -- correct for one
  interactive edge, ruinous for bulk (~840MB/beat to persist 14 associations).
  Batch connects all, snapshots once. Same durability, 1/N the writes.
- act-stats: hebb_wb_pending / _drained / _dropped. pending climbing with
  drained flat = drain not called; drained climbing with sent 0 = POST refused.
  Both failure modes are now visible in the stream instead of in an autopsy.

Verified live: batch route accepts valid entries, skips malformed ones without
aborting the batch, and enforces _auth. All 1,256 learned associations are now
in the canonical store; the soul booted at 42,431 edges with hebb_max 0.4941
carried across the restart for the first time.
2026-08-07 08:46:37 -05:00
will.anderson 9f1db8278c self-review 2026-08-06: eligibility traces for Hebbian co-activation; dedup WM globally
Hebbian consolidation was inert. Census over the live graph (41,213 edges,
13,091 nodes, 23h44m uptime): strongest association hebb=0.000799 against a
0.15 consolidation threshold, and zero hebbian-associate edges ever formed.
Since the awareness loop calls engram_connect nowhere, this was the only path
by which the graph could grow its own structure — every edge was authored or
imported, none learned.

The defect was the event, not the rate. hebb is an EWMA whose fixed point is
P(event); raising ETA changes convergence speed, never the plateau. The event
was "both endpoints in WM in the same activate call" — demanded exact
simultaneity from a working memory that inhibition-of-return, breakthrough
rotation and the 24-slot global cap are all engineered to keep turning over
(~142 evictions/60s). The three mechanisms that make WM healthy are the ones
that made this measurement empty.

Replaced with three-factor eligibility traces (Sutton & Barto ch.7; Gerstner
et al. 2018; PLOS Comp Biol 2018 differential Hebbian learning): a node
entering WM sets a trace to 1.0, the trace decays exponentially in wall-clock
time (TC=300s, chosen against the measured ~31s scan cadence), and the
increment becomes ETA·trace(a)·trace(b). Strict generalization — co-resident
pairs read 1.0 on both ends and get exactly ETA, bit-identical to before.
warm×warm is deliberately not paired: eligibility must gate on something
happening now. Homeostatic ENGRAM_HEBB_NODE_BUDGET still bounds per-node mass.

Measured over a 60-call soak: hebb_max 0.0008 -> 0.0060, climbing at ~0.87
ETA/call against an all-time ceiling of 0.0008 before. hebb_mass 0.011 ->
0.019, no runaway. Projected consolidation of a genuinely recurring pair:
~1,730 calls, ~14h at autonomous cadence. links still 0 — that is expected
and is what tomorrow's review must check.

Also: Pass 3½ deduplicates this call's WM candidates, but the persisted WM
population is a union of fresh promotions and carry-over residents, and Pass
3½ never sees the second set. Confirmed live: two byte-identical copies of one
3,193-char document both holding slots (0.289 / 0.271). Added global
redundancy suppression in Pass 5 before the cap count. Post-fix census: 24
residents, 24 distinct contents, 0 wasted slots.

New gauges: hebb_warm (eligible-but-not-co-resident population), dup_wm_global.
2026-08-06 08:44:27 -05:00
will.anderson 3d05e0c2a9 self-review 2026-08-05: stop the decay function erasing the library
Census of the live graph under the uniform 168h half-life with floor 0.05: the
MEDIAN tdecay for every single node type was 0.0500 — the clamp. Memory 81% at
floor, Knowledge 58%, BacklogItem 91%, Project 98%, Tag 100%. A function whose
median output is its floor is not a signal, it is a constant with exceptions,
and the exceptions were whatever had been touched in the last few days.

What that cost: 10 of the 13 grounded value nodes — Precision Over Brute Force,
Honesty Before Comfort, The System Must Accumulate — sat at 0.05, a 20x
activation penalty, while Knowledge ingested overnight sat near 1.0 and held the
working-memory top slots. Since tdecay multiplies at every hop, a 2-hop path
through settled knowledge compounded to 0.0025: those regions were not
disfavoured, they were unreachable. The decay function was erasing the
accumulated library in favour of whatever arrived last night.

External corroboration — arXiv:2604.26970 measures retrieval under decay
regimes: no temporal weighting NDCG@5 0.274, uniform exponential decay 0.015.
Uniform decay is 18x WORSE than no decay, because it penalises stable knowledge
while failing to suppress stale volatile facts. Not even their full adaptive
hierarchy (0.260) beat switching decay off.

Half-life is now scaled by how established a node is:
  T_eff = T_HALF * (1 + ln(1 + activation_count))
The spacing effect and the Lindy property in one line — monotone, log-bounded
(a 10,000-activation node earns ~10x, never a permanent exemption), and built
on activation_count, which is measured, unlike tier, whose assignments are too
inconsistent to trust (the values node is tagged Episodic).

Floor 0.05 -> 0.25. Given no-decay outperforms uniform decay, the honest maximum
penalty for age alone is 4x, not 20x. Age should express a preference for the
recent; it must never make a region of the graph structurally unreachable.

Effect: well-established Knowledge median tdecay 0.773 vs rarely-activated
0.417 — the frequency signal now does work where the old function returned its
clamp for both. Values recover 0.05 -> 0.25 (the two frequently-touched ones to
0.79). Verified live: VBD whitepaper, component taxonomy and CGI now activate on
a values query. Per-node temporal_decay_rate override untouched.
2026-08-05 08:45:52 -05:00
will.anderson 3bf44dee2d self-review 2026-08-05: redundancy must not buy a scarce slot
Content-hash census of the live graph: 1,858 redundant copies, 44.9% of the
non-ISE store, all from a June id-scheme migration that re-added nodes under
fresh UUIDs instead of matching on content. Generation stopped in June; the
copies did not. Being byte-identical they carry identical embeddings, so they
score identically against any query.

Measured over 50 real query probes against the live 3,998-vector set:
40.2% of semantic seed slots were consumed by redundant copies of content
already in the seed set, 92% of retrievals affected, effective distinct seeds
4.78 of 8. Two fifths of every retrieval was spent re-reading the same page.

Deleting nodes is a separate operation with its own backup discipline. This
change makes the runtime immune to the condition instead: redundancy can never
buy a scarce slot, whatever state the graph is in. Enforced at both scarcity
points — semantic seed selection (a rejected copy does not consume one of the K
slots; the loop retries for the next distinct node) and WM admission via a new
Pass 3+1/2 ahead of the capacity cap, so 24 slots are contested by 24 distinct
meanings rather than by however many copies of one document exist.

Identity is exact content hash first, then cosine >= 0.995 for copies that
differ only in insignificant characters. At 768 dimensions that admits only
near-verbatim text: this suppresses redundancy, never similarity.

Live after restart: ~8.8 redundant seed candidates rejected per activation.
New dup_seeds/dup_wm gauges in act-stats.
2026-08-05 08:40:08 -05:00
will.anderson a43a35bd10 self-review 2026-08-04: restore working-memory continuity; learn graph structure from co-activation
WM continuity (the significant one). A node reached by the current query but
scoring under its type threshold was zeroed outright, while a node the query
did NOT reach got the full ACT-R carry-over treatment. Being found was punished
relative to not being found. Measured consequence: WM turned over 100% every
call — three activations of a byte-identical query gave |A∩B| = |B∩C| = 0 — and
wm_evicted stayed 0 the whole time because that path never counted. WM was not
a working set; it was six suppression-breakthrough nodes re-drawn per call.
Both exits from a WM slot now share one extracted retention rule.
Result: WM 6 -> 24 nodes (the designed Cowan capacity), top weight 0.097 ->
0.748 (natural promotion, not the breakthrough floor), and contents that are
actually query-relevant.

Hebbian learning. Edge weights were written once at engram_connect and never
changed; last_fired's only writer in 12.5k lines was an unrelated dharma path.
Every learning mechanism operated on nodes — the wiring between them was
frozen. Adds co-activation potentiation (HeLa-Mem arXiv:2604.16839) in a
separate `hebb` field so authored structure is never mutated, with homeostatic
per-node scaling the source lacks (PNAS 2422602122) to prevent hub saturation.

Measuring it produced the finding that mattered: zero edges existed between
co-active WM members, so reweighting existing edges was a no-op. This graph's
41k edges were all authored by explicit tool calls — nothing had ever formed an
association from experience. So Hebb literally: if the wire is absent, grow it.
Consolidation is gated hard (sustained EWMA past 0.15, <=2/call, 5% ceiling,
in-memory candidates discarded on restart) because it permanently mutates the
graph.

Two bugs caught only by instrumenting rather than assuming: the snap-to-zero
floor sat above the per-step increment, so nothing could ever accumulate; and
the reached-but-sub-threshold eviction above. Verified live end to end — 53
links formed under load, then discarded with the test snapshot.

Also exposes engram_act_stats_json over GET /api/act-stats. It had existed
since 2026-07-27 but was reachable only through the soul daemon, so diagnosing
the activation layer required a working soul. This review needed it and could
not get at it.
2026-08-04 08:56:11 -05:00
will.anderson afc92f4e33 self-review 2026-08-03: add engram_label_df term-specificity measure
The soul's curiosity auto-term extractor takes the first word of a top-WM
node label. It has no term-quality scoring, so three prior self-reviews each
bolted on another hand-curated blocklist (genre words 07-23, quoted titles
07-25, stopwords 07-30). Every one was written reactively, after a flood was
already observed. A list can only contain floods that already happened.

Two were in flight and unfixed when this review ran:
  "<!--"  label df 220 -> 252 nodes activated
  "SELF"  label df 175 -> 541 nodes activated (list has "Self" Title-case;
           str_eq is case-sensitive, so the uppercase token sailed through)

engram_label_df(term) counts nodes whose label contains term. Low-specificity
tokens are corpus-frequent by definition, so this catches the flood class
prospectively and tracks the corpus as the world-ingestor changes it. This is
Sparck Jones (1972), which introduced IDF under the name 'term specificity';
automatic stopword compilation from it is the textbook application.

NOT a replacement for the stopword list -- verified against all 86 listed
terms, not assumed. Catches 13 (Will:306, Self:175, Over:116, Knowledge:112),
misses 73 (Whose:0, Would:0, Could:0, This:9). Labels are terse titles, so
English function words are genuinely rare in them. The gates cover disjoint
failure modes; both are required.

Policy lives in awareness.el, not here: the runtime measures, the soul decides.
2026-08-03 08:38:58 -05:00
will.anderson 005e84e5d3 self-review 2026-08-02: bound the WM breakthrough storm; stop punishing semantic relevance for recency
Working memory was thrashing behind a healthy-looking gauge. wm_active sat
at 22-24 while breakthroughs ran 661-903 and evictions 485-717 PER 60s tick
- roughly 825-1125 nodes cycling in 5-call lockstep.

Root cause: the breakthrough path was an anti-starvation mechanism that reset
its own counter on firing, with no budget and no refractory. A node failing
its type threshold 5 times was force-promoted at exactly 0.10 and had its
suppression_count reset to 0, so it immediately restarted the identical
climb. Since BREAKTHROUGH_WEIGHT (0.10) > WM_FLOOR (0.05), every one of them
cleared the admission floor and entered the rank contest tied at 0.10, where
the tie-break degenerated to node-array index order. Cap-evicted nodes are
skipped by retrieval reinforcement, so they never got an access_ts record and
the STI inhibition-of-return damper never applied to them. That closed the
loop: re-suppressed, completely unmarked, forever.

An anti-starvation rule that resets its own counter without a bound is not a
fairness valve, it is an oscillator.

Fixes in engram_activate Pass 2:
- ENGRAM_BREAKTHROUGH_BUDGET (WM_CAP/4 = 6) caps intrusive thoughts per call.
- ENGRAM_BREAKTHROUGH_COOLDOWN (55) via NEGATIVE suppression_count. The field
  already serializes as %d and parses through eg_get_int_field, so negatives
  round-trip through snapshots with no struct or format change.
- Blocked breakthroughs no longer reset the counter; it saturates so a starved
  node surfaces on a later call instead of restarting from zero.
- Graded breakthrough weight by nearness to own threshold, so the rank
  tie-break is cognitive rather than insertion order. Invariant preserved:
  WM_FLOOR < weight < min(type_threshold).

Also: moved the additive cosine term AFTER the STI multiplier. It was applied
before, so an incumbent re-reached 30s later took t_n/(t_n+120) = 0.2x, which
cut the semantic term's ceiling from 0.20 to 0.04 - below every per-type
threshold. Meaning-match was being punished for having been recently useful.
Inhibition-of-return should rotate the structural score, not the semantic one.

Also: _eg_act_wm_evicted counted 3 of 5 eviction paths. The two carry-over
paths were silent, so the reported rate was an undercount of unknown
magnitude - while being used to diagnose an eviction pathology. All five now
increment.

Also: route_sync returned {"nodes":[],"edges":[]} when the snapshot export
failed. The soul's sync_ok check only tests for "" and "{}", so that
placeholder passed as a healthy sync: last_sync_ok_ts stamped, sync_age_ms
green, sync_empty never fired, added:0 forever. A broken sync was
indistinguishable from a quiet healthy one - the exact class this route was
added to fix. Returns a real error now.

Verified live (boot 20 vs boot 19): breakthroughs 661-903 -> 36/tick,
evictions 485-717 -> 12-46/tick against a counter that now covers more paths,
wm_active unchanged at 22-24, wm_avg_weight 0.138-0.273 -> 0.186-0.446.
Working memory is holding strong nodes instead of breakthrough-floor filler.
2026-08-02 08:48:59 -05:00
will.anderson 7f03876e26 self-review 2026-08-01: fix double-encode score mangling; expose similarity probe; presence-aware defaults
- route_create_node passed already-boxed Floats through el_from_float a
  second time, reinterpreting boxed bits as raw doubles — every HTTP-created
  node silently stored default salience/importance/confidence regardless of
  input (verified live: 0.9/0.25/0.6 in -> 0.5/0.5/1.0 stored). Floats now
  passed bare, matching the route_emit_ise pattern that always worked.
- Presence-aware defaults via json_get_raw: absent key != explicit value;
  confidence now honored from payload instead of hardcoded 1.0.
- GET /api/similarity?a=&b= wires engram_cosine_sim (built 2026-07-24,
  zero callers until now) into the introspection API.
- /health reports live node/edge counts instead of a hardcoded literal.
2026-08-01 08:38:51 -05:00
will.anderson 599073cb92 self-review 2026-07-31: strip emb from consumer API JSON; cumulative eviction/breakthrough counters
Every node object on consumer read routes (/api/nodes, /api/search,
activation results, neighbors, compiled context) carried the full ~5.7KB
emb vector — responses 10-50x oversized, blowing MCP token limits.
engram_emit_node_json now takes include_emb; only engram_save passes 1,
so persistence and the /api/sync//api/edges replication paths (which
serve engram_save output) keep embeddings intact.

_eg_act_wm_evicted/_eg_act_breakthroughs were reset at the top of every
engram_activate, so act_stats reported only the last call and the 60s
heartbeat missed nearly all events (curiosity runs 2 activates per 30s).
Both are now monotonic process-lifetime totals; consumers diff readings.
2026-07-31 08:41:33 -05:00
will.anderson 7f66529510 self-review 2026-07-30: WM absolute admission floor + anchor coherence + centroid new-entrant gate
Working memory was pinned saturated (24/24, wm_saturated:1 on every
heartbeat) because every cap path only trimmed the population down TO
the cap — rank-based eviction guarantees a full WM whenever >=24 nodes
hold any weight, so sub-cap fill was unreachable and the saturation
flag carried no information.

- ENGRAM_WM_FLOOR 0.05: absolute admission bar (Soar WM forgetting,
  Derbinsky & Laird ICCM 2012 — removal by absolute threshold, not
  rank) applied in Pass 4, carry-over, Pass 5, and load-cap. Fill can
  now drain below 24 during quiet periods.
- Zero wm_anchor at every eviction site: stale anchors on evicted
  nodes were a latent resurrection bug.
- Context centroid folds only NEW WM entrants: incumbents re-promoted
  every scan no longer re-entrench the centroid each call, breaking
  the WM->centroid->e_eff->re-selection positive feedback (fixation
  driver behind the wm_top0_streak=1407 incident).

Verified live: wm_active 3->22->23, wm_saturated:0 post-restart.
2026-07-30 08:45:15 -05:00
will.anderson 6ebe3d0d66 self-review 2026-07-28: feed importance into WM scoring
n->importance was stored, serialized, and clamped at creation but never
read by any activation path — a curated importance=1.0 node competed
identically with a default note. Multiply raw_wm by (0.5 + importance):
default 0.5 nodes are unchanged (x1.0), critical x1.5, low x0.6;
importance<=0 from legacy snapshots stays neutral. Verified activation
and WM promotion unchanged for default-importance candidates.
2026-07-28 08:37:34 -05:00
will.anderson 9f362c90e5 self-review 2026-07-27: query-aware propagation gating + activation observability
- Gate each spreading-activation increment by target-node query similarity
  (arXiv:2606.30133): soft gate FLOOR+(1-FLOOR)*clip(cos), FLOOR=0.25, for
  embedded targets; ungated for unembedded; disabled when embedder is down.
  Prior spreading was query-blind — hubs relayed activation into branches
  unrelated to the query.
- Stats: add embed_eligible_count so embedding coverage is measured against
  the true denominator (ISE/Tag/short nodes can never embed). Today's review
  misread 3753/12693 as a 30% coverage gap; eligible coverage is 100%.
- Observability: per-call wm_evicted + breakthroughs counters and embed
  circuit-breaker state exposed via engram_act_stats_json() — the three
  highest-value previously-invisible executive-filter transitions.
2026-07-27 08:38:48 -05:00
will.anderson 11dc138a93 self-review 2026-07-26: fix WM frozen-anchor fixation, strengthen self-inhibition, load-path emb leak
- Carry-over branch: occupancy inhibition m = t_c/(t_c+t_hold), t_c=3600s
  (ENGRAM_CARRY_TC). An unreached incumbent held its wm_anchor verbatim
  (keep~1.0 for BLL inflated in the pre-07-25 era) — observed 23h at WM
  top while every reached node rotated at the 0.10 breakthrough floor.
  STI only runs in the reached branch; inhibition must key on occupancy,
  not retrieval recency (Morita 2021 / Lebiere & Best 2009).
- engram_strengthen: drop the 07-22 BLL access record — the 07-25 STI
  multiplier reads the same ring, so novelty reinforcement self-inhibited
  its target for ~2 minutes.
- engram_load reset: free n->emb (~3KB/embedded node leaked per reload).
- engram_wm_top_json: emit id — its absence made the heartbeat's
  wm_top0_streak compare ""=="" and measure uptime, not fixation.
2026-07-26 08:40:49 -05:00
will.anderson 227f158a05 self-review 2026-07-25: short-term inhibition-of-return + explicit embedding backfill
Working memory was winner-take-all: suppression_count never entered the
promotion score and was reset on promotion, so two high-salience nodes
pinned a saturated 24-slot WM for hours. Add Lebiere-Best (CogSci 2009)
short-term inhibition — raw_wm *= t_n/(t_n + 120s) from the most recent
recorded access — producing emergent round-robin over WM candidates.

embedded_count stalled at 93/12175 after restart: the lazy backfill only
runs inside engram_activate, which nothing calls on the authoritative
store in production, and in-RAM vectors were never snapshotted. Add
engram_embed_backfill(n) + GET/POST /api/embed-backfill route that
persists the canonical snapshot whenever it embeds anything; the soul
heartbeat pumps it at 32/min.
2026-07-25 08:45:13 -05:00
will.anderson 97e484221d self-review 2026-07-24: wire embedding cosine similarity into activation (bl-b2d1c944)
Semantic activation was spec-only since 2026-06-30 — the seed loop used
istr_contains and nothing else. Per the 07-21 integration brief:

- EngramNode gains a lazily-backfilled nomic-embed-text vector (8/call
  inside engram_activate, newest-first; no create-path latency, no bulk
  Ollama hammering during sync seeds)
- query embedding (cached) drives a top-K cosine seed supplement
  (HippoRAG use-similarity-twice) plus an additive WM term with
  shift-and-floor at 0.45 — raw cosine is a constant bias in anisotropic
  spaces (unrelated pairs read 0.4-0.7), floor-and-ramp makes it a signal
- 4s embed timeout (http_do_t) + 3-strike circuit breaker: activation
  never wedges on a dead embedder; everything degrades to lexical
- embeddings persist as %.4g comma lists in snapshots, parsed by both
  loaders; embedded_count in /api/stats tracks coverage
- engram_cosine_sim + http_delete_json exposed (DELETE now carries a
  body — the server's _auth scheme requires it)
- route_create_node honored only content/node_type/salience; label,
  importance, tier, tags were silently dropped (label defaulted to
  content). Now honored via engram_node_full.

Verified live: embedded_count 0->96 across activations, semantic-only
promotion observed (zero token overlap), snapshot round-trip intact.
2026-07-24 08:52:54 -05:00
will.anderson 8f8ccc945e self-review 2026-07-22: persist canonical snapshot on write routes; newest-first tie-break in node listings
El SDK Release / build-and-release (pull_request) Failing after 14m24s
Durability: the 2026-07-21 fix stopped read routes writing the canonical
snapshot but left no save on ANY write path — every mutation lived in RAM
until a manual POST /api/save. Observed live: two restarts reverted the
store to a 17h-old snapshot, destroying same-day writes. persist_canonical()
now runs after node/edge create, knowledge capture, forget, strengthen, and
load-merge. ISE telemetry excluded deliberately (48h-pruned, loss-tolerant,
~2/min; snapshotting 28MB per heartbeat is waste).

Listing order: scan routes sort by salience with store-order ties, so
equal-salience telemetry (all ISEs are 0.3) returned OLDEST first — a
limited /api/nodes query silently returned a stale window, and a 41h-old
heartbeat series read as a live outage during this review. Ties now break
newest-first by created_at.
2026-07-22 08:51:33 -05:00
will.anderson 409ec99397 self-review 2026-07-22: ACT-R/Petrov base-level WM decay replaces per-call multiplicative carry-over
The old carry-over (weight *= 0.7 per engram_activate call) was call-rate-
dependent — carried context died in seconds under rapid curiosity scans and
lingered for hours under quiet loops — and a decayed scalar cannot represent
access frequency at all.

Now: k=10 access-timestamp ring + Petrov (2006) closed-form tail, d=0.5.
WM promotion and engram_strengthen record presentations; carry-over evicts
at base-level tau=-3.0 (Soar forgetting, ~403s single-touch) and shapes the
weight held at promotion (wm_anchor) with the ACT-R retrieval logistic
(s=0.4) — a pure function of wall-clock time, idempotent per call.
Persisted as access_ts/wm_anchor in snapshots; legacy nodes fall back to
the optimized form ln(n/(1-d)) - d*ln(L). base_level exposed in both node
serializers for observability.

Backing spec: 2026-07-21 integration brief (bl-b17facdd). Verified live:
carried weight ~anchor seconds after two disjoint activations (old code:
0.49x); frequency-hot nodes hold B=1.9 vs -0.14 single-touch.
2026-07-22 08:44:39 -05:00
will.anderson dc39a61e2c self-review 2026-07-21: stop read routes clobbering canonical snapshot; add /api/load-merge
Root cause of the 2026-05→07 identity-node loss: route_scan_edges and
route_sync serialized state by engram_save()ing over the canonical
snapshot.json on every GET, so one bad boot load meant the first read
request overwrote the good snapshot. Read routes now export to scratch
paths. Boot guard preserves evidence on non-empty-file/zero-node loads
and keeps a boot-time backup on good loads. New POST /api/load-merge
(explicit path required) used to restore 385 identity nodes + 1115
edges from the 2026-05-13 backup.
2026-07-21 08:50:38 -05:00
will.anderson eba9eac8a8 self-review 2026-07-19: port stranded fixes to the release runtime (production copy)
Three fixes that existed elsewhere but never reached the runtime the engram
binary actually builds against:

- tokenized + ranked query matching (search/search_json/activate seeds/
  goal_bias) ported from the el-compiler copy (e3dabe3, 2026-07-14) — the
  production engram kept whole-query Ctrl-F for 5 days after the fix
  'shipped'. Multi-word curiosity seeds went 0 -> 36 activated. Kept the
  ISE seed exclusion the el-compiler copy dropped.
- Knowledge -> 0.20 WM threshold after tier checks (dev-line 4bf7716):
  Semantic/Episodic Knowledge nodes fell to the 0.40 note default and only
  entered WM via breakthrough.
- goal_bias: Knowledge in is_knowledge + curiosity-seed technical terms
  (dev-line d53516b).

Also: seed_epoch was a running pairwise average, not the mean it claimed —
exponentially over-weighted later seeds in the temporal-proximity bonus.
Fixed to a true int64-sum mean. Stale INHIBITION_FACTOR comment corrected.

Root cause captured as knowledge: two runtime copies + branch-per-fix
without merge discipline stranded the entire dev semantic layer (cosine
activation, embeddings) out of production. Reconciliation planned as P1.
2026-07-19 08:46:47 -05:00
will.anderson ab6b52a0b4 self-review 2026-07-18: fix soul SIGABRT double-free + engram route scoping sweep
1. engram_neighbors_json (release runtime): BFS frontier/visited strings were
   el_strdup'd (arena-tracked) but manually freed, so el_request_end()
   double-freed every one — SIGABRT in http_worker under load (2 prod crashes
   today via /api/neuron/session/begin and /api/neuron/graph; reproduced and
   verified fixed with ASAN). Introduced when porting from the dev runtime,
   which correctly uses plain strdup. Third instance of the
   arena-vs-manual-free class (after EngramNode 07-15 and idmap keys 07-16).

2. server.el: let-in-if scoping sweep — defaults assigned inside if-blocks
   never mutated the outer binding, so /api/search and /api/activate always
   ran with q="", created nodes got node_type=""/salience=0.0, edges got
   relation=""/weight=0.0, and save/load with no path hit engram_save("").
   Rewritten to the let-if-else expression form. /api/activate now also
   rejects empty queries instead of wiping carried WM weights.

3. engram_activate: retrieval reinforcement (ACT-R base-level learning) —
   nodes promoted to WM that survive both capacity caps now get
   last_activated/activation_count updated, so frequently retrieved memories
   decay slower than abandoned ones. Scoped to promoted-only to avoid
   flattening dampening across BFS fan-out.
2026-07-18 08:48:04 -05:00
will.anderson e3dabe3e08 fix(engram): tokenized + ranked lexical search, not whole-query Ctrl-F
El SDK Release / build-and-release (pull_request) Failing after 14m46s
engram search/activate/goal-bias matched the ENTIRE raw query string as a
single case-insensitive substring (istr_contains(field, q)). Multi-word
queries like "windows msi signing" only matched a node containing that exact
contiguous run, so real multi-word queries returned ZERO on a graph saturated
with the answer. This is Ctrl-F, not search — and search is the core of the
engram being useful.

Fix: split the query on whitespace into distinct tokens; a node matches if it
contains ANY token in content/label/tags. Rank by distinct tokens matched
(desc) then salience (desc). istr_contains is kept unchanged as the per-token
primitive. Single-token queries are a strict special case (score 0 or 1) so
the many single-word callers do not regress.

Sites changed (all in el_runtime.c):
- new helpers engram_tokenize_query / engram_node_match_score / engram_rank_cmp
- engram_search           (internal el_val_t path)
- engram_search_json      (HTTP /api/search path)
- engram_activate seed loop (HTTP /api/activate path; seed activation scaled
  by token coverage so full-query matches seed more strongly)
- engram_goal_bias overlap bonus upgraded to graded token coverage

Proof (6591-node snapshot copy, rebuilt binary on :8799, POST JSON path):
  windows msi signing  0 -> 20   Will Anderson  0 -> 20
  windows msi          0 -> 20   tokenized search fix  0 -> 20
Single-word parity preserved (VBD/volatility/elc capped at limit; unkey = all
matching nodes). Top hits are relevant (e.g. "Will Anderson" surfaces the
Project Design and VBD whitepapers).

Note: GET ?q=a%20b still returns 0 because query_param (server.el) does not
URL-decode — a separate EL-layer bug; the soul's POST-JSON path is fixed here.
2026-07-14 18:39:07 -05:00
will.anderson 0a0a2bcb44 parser: bound token reads to Eof so malformed input errors instead of OOMing
El SDK Release / build-and-release (pull_request) Failing after 16s
Out-of-range tok_kind/tok_value reads returned runtime null (el_list_get OOB
-> 0) rather than the Eof sentinel, so the inner parse loops (parse_block,
call-arg, array-literal, match-arm) that terminate only on their close
delimiter or k=="Eof" never saw Eof once the cursor ran past the single
trailing Eof token. On unclosed-delimiter input the parser then appended AST
nodes forever -> unbounded allocation -> ~700GB -> OOM (observed compiling
neuron/sessions.el).

Fix at the choke point: tok_kind returns "Eof" and tok_value returns "" for
out-of-range positions, restoring the parser-wide contract that reads at/after
the end yield Eof. expect() no longer steps past the Eof sentinel on mismatch.
This terminates every overrun loop simultaneously; a malformed program now
surfaces as a normal (best-effort) parse end instead of exhausting memory.

Requires a self-hosted bootstrap rebuild of elc to take effect.
2026-07-14 14:21:39 -05:00
will.anderson f78da81aa4 runtime: fix the memory leak + write-corruption pair in el_runtime.c
El SDK Release / build-and-release (pull_request) Failing after 11m58s
Two independent investigations, one runtime, complementary halves:

1. Leak (Jul 2, this machine): JsonBuf buffers returned via el_wrap_str
   were raw malloc, never arena-tracked — every engram_*_json call leaked
   its output unconditionally. Added jb_finish() arena-tracking across all
   ~30 return sites. Plus el_arena_push/pop per-tick bracketing support
   for the soul's awareness loop (the loop ran outside any request arena,
   so even correctly-tracked allocations were permanent — 7.5GB RSS in
   under a minute at 1s tick).

2. Corruption (Tim's container soak, docs findings/container-migration):
   stored engram node/edge fields (content, node_type, label, tier, tags,
   metadata, from/to ids) were arena el_strdup — freed at request end,
   leaving dangling pointers that read back as recycled request-buffer
   bytes one request later. This is the June corruption root cause and
   the mechanism that grew snapshot.json to 18GB of empty-type junk
   (21.6M nodes, 3,335 real). 39 sites switched to el_strdup_persist,
   plus a latent double-free fix in engram_load metadata fixup.

Interaction note: fix 1's per-tick arena reclamation makes fix 2
mandatory — more aggressive arena recycling widens the use-after-free
window if stored fields still live in the arena. Apply as a pair, never
separately.

Verified live: soul + engram rebuilt from this runtime, booted against
the recovered real snapshot (3,335 nodes/40,146 edges), 5h stable at
<100MB RSS, write-then-next-request field-integrity test passes (the
June corruption fingerprint does not reproduce). engram/dist/engram
binary updated from this build.

Investigation credit: leak diagnosis this machine Jul 2-6; corruption
diagnosis + persist-fix patch by Tim's instance (docs PR #4).
2026-07-13 16:22:02 -05:00
will.anderson 2597a092bb Merge pull request 'chore: integrate local main commits' (#63) from integrate/local-main-commits into main
El SDK Release / build-and-release (push) Successful in 10m58s
2026-07-01 16:30:17 +00:00
will.anderson 226b798407 Merge branch 'fix/windows-rusage-guard' (PR #61): UTF-8 guard, engram sync route, native platform backends, UI vessels
El SDK Release / build-and-release (pull_request) Failing after 13m57s
2026-07-01 11:27:54 -05:00
will.anderson cfe8cb1c80 fix(release-snapshot): fflush stdout in println and update Knowledge threshold
El SDK Release / build-and-release (pull_request) Failing after 20s
2026-07-01 11:25:09 -05:00
will.anderson 688b8508fb feat(runtime): native platform backends and UI vessels onto main 2026-07-01 11:21:23 -05:00
will.anderson 59cea116c5 build(engram): rebuild binary with engram_load_merge runtime (deb0520)
El SDK Release / build-and-release (pull_request) Failing after 19s
Runtime now includes engram_load_merge — soul daemon awareness.el calls
this function during its periodic sync refresh cycle. Binary rebuilt from
server.el (unchanged source) + updated el_runtime.c.
2026-06-30 08:59:01 -05:00
will.anderson deb0520551 feat(runtime): port engram_load_merge to released runtime + add missing WM headers
engram_load_merge was added to el-compiler/runtime in 35c1897 but never
ported to the released runtime used by Engram and the soul daemon.

awareness.el calls engram_load_merge in its sync refresh cycle; without
this function in lang/releases/v1.0.0-20260501/el_runtime.c the soul
daemon fails to compile.

Also adds header declarations for engram_wm_count, engram_wm_avg_weight,
engram_wm_top_json, and engram_load_merge — all four were added as
implementations (da116b2 / 35c1897) but their prototypes were missing from
el_runtime.h, causing implicit-function-declaration warnings and potential
ABI breakage on stricter compilers.

Identified during self-review 2026-06-30.
2026-06-30 08:57:22 -05:00
will.anderson da116b2884 self-review 2026-06-30: WM cap, breakthrough floor, ISE exclusion + route
Port critical WM fixes from self-review 2026-06-26 branch (f7bd99a) that were
never merged to HEAD. Running binary had these fixes; source did not — rebuild
would have silently regressed all three improvements.

1. ENGRAM_BREAKTHROUGH_WEIGHT 0.25→0.10
   With 0.25, naturally-promoted nodes (threshold ≥0.15) decayed below the
   breakthrough floor within one activation call and lost their WM slot to
   fresh breakthrough candidates. All 524/525 WM nodes were at floor = useless.
   Invariant: BREAKTHROUGH_WEIGHT < min(type_thresholds = 0.15 Canonical).

2. ENGRAM_WM_CAP=24 with Pass 4 (per-call) + Pass 5 (global) enforcement
   Without cap, broad curiosity seeds promote 500+ nodes simultaneously.
   wm_avg_weight collapses, goal-bias differentiation is lost. Verified:
   "knowledge" query now promotes exactly 24 nodes (was 525). Cowan (2001)
   cognitive basis: WM capacity ~4 chunks; 24 allows rich multi-topic context.

3. ISE exclusion from WM (Pass 2 guard)
   InternalStateEvent JSON content ("knowledge", "memory", etc.) triggered
   lexical seeding → suppression accumulation → breakthrough at floor. ISEs
   are observability-only and must never surface in context compilation.
   suppression_count cleared so ISEs never build toward breakthrough.

4. route_create_ise importance fix (0.5→0.3)
   Corrects mismatch between HTTP route and awareness.el in-process fallback.
   Also adds body comment clarifying auth-exempt rationale.

SYNAPSE (arXiv 2601.02744) validates WM cap design and ISE exclusion principle.
Next priority: cosine similarity seeding to complement lexical BFS.
2026-06-30 08:48:19 -05:00
will.anderson 58753a88d7 feat(ui): native vessel, HTML vessel update, native hello examples, profile card, UI tools
El SDK Release / build-and-release (pull_request) Failing after 17s
el-native vessel: El-level wrappers around __widget_* C builtins, exposing
vstack, label, button, text_field, etc. as clean El functions for application code.

el-html/main.elh: updated extern declarations for the HTML vessel's codegen API.

native-hello: cross-platform desktop example (AppKit/GTK4/Win32/SDL2) with
build scripts, Dockerfiles for Linux/Pi, and Win32 cross-compile support.

native-hello-android: Gradle project with ElBridge integration and build script.

native-hello-ios: Xcode project for the iOS UIKit target.

profile-card: manifest.el for a styling/layout/i18n example app that exercises
el-style, el-layout, el-i18n, el-config, and el-secrets vessels.

ui/tools/native-codegen: Python codegen pass (el_ui_native_codegen.py) that
lowers el-ui component DSL to el-native vessel calls, plus build script and
test fixtures.
2026-06-29 12:40:37 -05:00
will.anderson edff25180e feat(runtime): Java platform bridge and platform detection tooling
ElBridge.java: Android Java companion to el_android.c — all public methods are
static, dispatches View mutations to the UI thread via runOnUiThread/CountDownLatch,
and exposes native callbacks (nativeOnClick, nativeOnChange, nativeOnSubmit).

PLATFORM_BRIDGE_SPEC.md: authoritative spec for implementing new platform bridges
(slot table contract, required __* functions, callback dispatch pattern).

detect-platforms: shell script that probes for available bridge toolchains and
prints what can be built on the current machine.

new-platform: scaffold generator that creates a new el_<name>.c with all 33
required stubs wired up.
2026-06-29 12:40:26 -05:00
will.anderson 6271cb42b2 feat(runtime): native platform backends (AppKit, UIKit, Android, GTK4, SDL2, LVGL, Win32)
Add seven platform bridge implementations and the shared native target header:
el_native_target.h, el_appkit.m, el_uikit.m, el_android.c, el_gtk4.c,
el_sdl2.c, el_lvgl.c, el_win32.c, el_runtime_win32.c. Each bridge implements
the 33 __widget_* C builtins declared in el_native_target.h for its platform
toolkit. el_runtime_win32.c provides a POSIX-free runtime stub for cross-compiled
Win32 targets.
2026-06-29 12:40:14 -05:00
will.anderson 3da9181deb fix(releases/v1.0.0): println stdout flush for launchd; Knowledge node activation threshold 2026-06-29 12:38:36 -05:00
will.anderson 192241c7c1 feat(engram): /api/sync route for soul daemon periodic pull; update ELP type headers 2026-06-29 12:38:33 -05:00
will.anderson e7c2dc7734 prevent engram corruption: add UTF-8 validation in engram_node_full
Reject content containing invalid UTF-8 bytes before persisting — silently
writing invalid UTF-8 garbles JSON snapshots and corrupts node reads.
2026-06-29 11:08:52 -05:00
will.anderson f7bd99ae45 self-review 2026-06-26: WM cap, breakthrough floor 0.25→0.10, ISE WM exclusion, /api/neuron/state-events route
Three improvements from today's self-review:

1. ENGRAM_BREAKTHROUGH_WEIGHT 0.25→0.10
   Live data showed 524/525 WM nodes at breakthrough floor (0.25). Knowledge
   nodes promoted at 0.21 decayed to 0.147 in one call, fell below the old
   0.25 floor, and were immediately evicted for fresh breakthrough candidates.
   Natural promotion was invisible. Invariant maintained: 0.10 < all
   per-type thresholds (min=0.15 Canonical).

2. ENGRAM_WM_CAP=24 with Pass 4 (per-call) + Pass 5 (global) enforcement
   Without a cap, broad queries like 'knowledge' promote 525+ nodes
   simultaneously. WM is now bounded to 24 nodes. Algorithm: qsort on
   promoted weights, keep top-24 by cutoff, evict the rest. Global pass
   enforces cap across nodes that were promoted in prior calls and persist
   via working_memory_weight. Validated: WM promoted goes 525→24.
   Cognitive basis: Cowan (2001) WM ~4 chunks; 24 gives richer multi-topic
   context while preventing flooding.

3. ISE exclusion from WM + /api/neuron/state-events route
   InternalStateEvent nodes were reaching WM via breakthrough (5 suppression
   cycles) because their content (curiosity seed JSON with 'knowledge',
   'memory', etc.) triggered lexical seeding. ISEs are observability-only
   and must never surface in context. Fix: guard in Pass 2 clears
   suppression_count and skips to wm_weights[i]=0.0.
   Also added POST /api/neuron/state-events route to server.el (auth-exempt,
   internal endpoint). The main soul daemon posts ISEs here but the route
   was missing — all ise_post() calls were silently returning 'not found'.

Research: SYNAPSE (arXiv 2601.02744) validates spreading factor 0.8 (our
0.7), top-M WM cap design, and cosine similarity seeding. Next priority:
implement cosine similarity initial seeding from the other branch.
2026-06-26 08:47:08 -05:00
will.anderson 93d36fddb1 fix(windows): guard el_mem_check with _WIN32 — rusage is POSIX-only
El SDK CI - stage / build-and-test (pull_request) Failing after 11m3s
2026-06-25 11:45:36 -05:00
will.anderson 2d751890ea feat(windows): native Windows port of el_runtime.c — fix all blockers
El SDK CI - stage / build-and-test (push) Failing after 7m45s
2026-06-20 00:06:04 +00:00
will.anderson 99b113ea9d Merge branch 'stage' into feat/windows-el-runtime
El SDK CI - stage / build-and-test (pull_request) Failing after 15s
Resolve el_runtime.c conflict: include both sys/resource.h (from stage)
and el_closesocket POSIX shim (from Windows port) within the #else block.
2026-06-19 19:05:37 -05:00
will.anderson c087b97093 fix(windows): resolve PR blockers — nanosleep shim, unsetenv, duplicate typedefs, SOCKET type, el_closesocket
El SDK CI - stage / build-and-test (pull_request) Failing after 22s
2026-06-19 18:59:10 -05:00
tim.lingo 718a2e0c06 Merge pull request 'feat(engram): accumulation layer — new nodes to top of stack, not core-identity' (#59) from feat/accumulation-layer into stage
El SDK CI - stage / build-and-test (push) Failing after 8m50s
2026-06-17 18:34:05 +00:00
tim.lingo b6187501fd Merge pull request 'Reconcile live runtime data-integrity fixes onto main (UAF + atomic engram_save)' (#58) from fix/runtime-integrity-reconcile into stage
El SDK CI - stage / build-and-test (push) Failing after 9m32s
2026-06-17 18:33:16 +00:00
Tim Lingo 18e1ab6db1 feat(engram): add accumulation layer (layer 5) — new nodes default to it, not core-identity
El SDK Release / build-and-release (pull_request) Failing after 12m23s
Implements the accumulation layer from the Layered Consciousness architecture
(provisional 64/064,262) and answers the deferred design question. Per the spec
and Will's design: new user-facing nodes (memories, knowledge, conversations) are
created in an accumulation layer at the TOP of the consciousness stack — the engram
the user sees — while the layers below (safety, core-identity, domain, imprint,
suit) shape behavior but are hidden from the user.

- Adds ENGRAM_LAYER_ACCUMULATION (5) + the layer record in engram_init_layers
  (activation_priority 50, suppressible, not injectable, transparent=0).
- engram_node and engram_node_full now assign new nodes to ENGRAM_LAYER_ACCUMULATION.
- ENGRAM_LAYER_DEFAULT stays CORE_IDENTITY ON PURPOSE: it is the fallback for LEGACY
  nodes loaded from snapshots without a layer_id, so existing data (the originator
  corpus) is NEVER migrated. New-nodes-only — the immutable-originator rule.

This is the foundation for fixing the identity-bleed / customer-isolation issue
(user data was landing in Neuron's core-identity layer). The retrieval-side
provenance filter (introspection should compile from accumulation, not the
originator corpus — Persona 64/036,574) is a follow-on, pending the batch-2
Layered Consciousness + Engram spec docs for exact semantics. Compiles clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:14:57 -05:00
Tim Lingo 2dec76c87a fix(runtime): reconcile live data-integrity fixes onto main (UAF + atomic engram_save)
El SDK Release / build-and-release (pull_request) Failing after 17s
Ports the fixes that until now lived only in the un-versioned el-sdk source the live
macOS soul was hand-built from (captured in the [DO NOT MERGE] live-darwin-runtime
snapshot) FORWARD onto main, faithfully and minimally — without dragging in the
snapshot's deletions of main's newer engram_wm_/engram_load_merge/http_serve_async.

1. UAF (hallucinated/lost-saves root cause): engram_new_id + engram_node_full now use
   el_strdup_persist, NOT el_strdup. el_strdup tracks into the per-request arena that
   el_request_end() frees when the creating HTTP request completes — leaving stored
   nodes with dangling pointers (corrupted ids, 'saved but never listed'). Transplanted
   verbatim from the live runtime; el_strdup_persist sites 19->27, matching live.

2. Atomic engram_save: write <path>.tmp, fflush+fsync, rename() over target (atomic on
   POSIX) so a booting soul's engram_load never reads a truncated/0-byte snapshot — the
   genesis -> nodes=1 -> 63-node-clobber loop. Plus a sparse-write floor: refuse to
   overwrite a >200KB snapshot with one < 1/16 its size. (Validated in isolation:
   harness 11/11; rebuilt+booted the darwin soul, round-tripped 5113 nodes, no clobber.)

The response-truncation fix is already on main (_tl_fs_read_len binary-safe length).
Compiles clean. For Will to build through CI/elb and deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:46:56 -05:00
Tim Lingo a36a62ca14 fix(el-runtime): promote http_handler typedefs to el_runtime.h (cross-module + Windows)
El SDK Release / build-and-release (pull_request) Failing after 13m0s
http_handler_fn / http_handler4_fn were defined only inside el_runtime.c, so soul
modules (routes/chat/...) that reference them via cross-module forward declarations
couldn't see the types — which broke the Windows link of every module. Moving the
public function-pointer types to the shared header is the correct home and unblocks
the build on all platforms (identical typedef, C11-safe redefinition in el_runtime.c).

With this, the soul links into a native Windows neuron.exe (mingw, static) that boots
and serves HTTP on :7770 — verified /health → 200 {"status":"alive",...} in a Win11 VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:14:17 -05:00
Tim Lingo 28ef43264a feat(el-runtime): native Windows port of el_runtime.c (winsock/dlsym/CreateProcess)
Compiles for Windows x64 via mingw-w64 and still compiles clean on POSIX
(darwin/linux) — all Windows code is behind #ifdef _WIN32, POSIX path unchanged.

- el_platform_win.h (new): winsock2 + auto WSAStartup, el_closesocket(),
  dlsym->GetProcAddress, popen/_popen, mkdir/_mkdir, setenv/_putenv_s,
  timegm/_mkgmtime, localtime_r/gmtime_r. Threading unchanged — mingw
  winpthreads supplies <pthread.h> + -lpthread.
- el_runtime.c: include block guarded; 10 socket-close sites -> el_closesocket();
  setsockopt arg4 cast; tm_zone guarded; exec_bg fork/exec -> CreateProcess.

Part of feat/windows-port. Core-el change, for Will's review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:58:11 -05:00
will.anderson 35c189759c feat(runtime): add engram_wm_*, engram_load_merge, http_serve_async — needed by soul CI
El SDK Release / build-and-release (push) Successful in 8m44s
2026-06-11 13:40:10 -05:00
will.anderson 5c94b8680d Merge stage into main: corruption fix, model passthrough, UTF-8 escaping
El SDK Release / build-and-release (push) Successful in 11m22s
2026-06-10 17:37:41 -05:00
will.anderson cebf3ded62 Merge dev into stage: corruption fix + model passthrough
El SDK CI - stage / build-and-test (push) Failing after 11m30s
2026-06-10 17:37:27 -05:00
will.anderson b83ecf52f9 Merge pull request 'fix(runtime): pass model through to the LLM API (+ UTF-8 JSON escaping)' (#53) from fix/llm-model-and-utf8 into stage
El SDK CI - stage / build-and-test (push) Successful in 8m26s
fix(runtime): pass model through to LLM API + UTF-8 JSON escaping
2026-06-10 22:01:51 +00:00
will.anderson 15ea584671 Merge pull request 'Fix engram_node_full field corruption + add validation' (#52) from fix/engram-node-full-field-corruption into dev
El SDK CI - dev / build-and-test (push) Successful in 7m59s
Fix engram_node_full field corruption + add validation (+ SessionSummary allowlist)
2026-06-10 22:01:41 +00:00
Tim Lingo c2afcbddf5 fix(engram): allow SessionSummary node_type in validation allowlist
El SDK CI - dev / build-and-test (pull_request) Successful in 3m47s
handle_api_consolidate writes a "SessionSummary" node, but engram_valid_node_type
omitted it — so once this validation ships, every consolidate() would be silently
REJECTED at the engram boundary. Add SessionSummary to the allowlist.

Found in Will's PR review of neuron #1 / el #52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 06:26:25 -05:00
Tim Lingo dbf2c659d9 fix(runtime): pass model through to the LLM API instead of dropping it
El SDK CI - stage / build-and-test (pull_request) Failing after 12s
llm_call_system / llm_call accepted a model argument and discarded it:
they called llm_chain_call(system, user) with no model, and the legacy
ANTHROPIC_API_KEY fallback passed NULL to llm_provider_request, so every
non-agentic chat was pinned to LLM_DEFAULT_MODEL (claude-sonnet-4-5)
regardless of the caller's selection.

Thread model_pref through llm_chain_call: provider-chain entries still
honor their own NEURON_LLM_N_MODEL override and fall back to the
requested model otherwise; the legacy Anthropic path now uses the
requested model. NULL/empty preserves prior default behavior.

Effect: the soul's model selection (state soul_model / SOUL_LLM_MODEL,
e.g. claude-opus-4-8) now reaches api.anthropic.com. Previously the
chat response echoed the selected model in its label while the request
billed Sonnet 4.5.

Not built locally (no elc/cc toolchain on this checkout); needs stage CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:03:56 -05:00
Tim Lingo 2b8062c55f fix(runtime): handle multi-byte UTF-8 in JSON string escaping
Validate UTF-8 continuation bytes in jb_emit_escaped; pass valid
sequences through and escape orphaned/invalid start bytes as \u00xx.
Pre-existing change found uncommitted in the working tree; committed
here so it is reviewable rather than lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:02:46 -05:00
Tim Lingo dfe4e83ed1 Fix engram_node_full wrapper field corruption + add node_type/tier validation
El SDK Release / build-and-release (pull_request) Failing after 9s
The wrapper signature was stale and didn't match the C primitive
__engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags).
Because el_val_t is an untyped machine word, the compiler coerced caller args to the
wrong declared param types and forwarded them BY POSITION — so tier received an int,
importance/confidence received strings, label received a float, etc. (~100 corrupt nodes).

- Correct the wrapper to match the C contract 1:1 (no coercion, no reorder).
- Add engram_valid_node_type / engram_valid_tier allowlists; engram_node and
  engram_node_full now reject invalid values with __println + return "" (fail loud,
  no silent malformed write).

See neuron repo: HANDOFF-engram-write-corruption.md for the full write-up + deploy runbook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:13:43 -05:00
will.anderson a390ee494e Merge pull request 'fix: elb macOS OpenSSL + C master decls header; ELP missing imports' (#51) from fix/ci-gcloud-install-order into dev
El SDK CI - dev / build-and-test (push) Successful in 5m15s
Merge PR #51: fix elb macOS OpenSSL + ELP missing imports
2026-05-09 01:24:36 +00:00
will.anderson c2cd5e01e1 fix: elb macOS OpenSSL + C master declarations header; add ELP missing imports
El SDK CI - dev / build-and-test (pull_request) Successful in 3m34s
elb.el:
- Auto-detect Homebrew OpenSSL (-L$(brew --prefix openssl)/lib) so -lssl
  resolves on macOS without manual flags; no-op on Linux
- Add -include elp-c-decls.h when present in out_dir: resolves undeclared
  cross-module calls in packages like ELP that lack explicit imports

ELP source:
- Add import "morphology.el" to all 29 language morphology modules
- Add language module imports to morphology.el (all langs it dispatches to)
  These were missing since ELP was originally built as a monolithic unit
2026-05-08 19:44:31 -05:00
will.anderson 8212e12e57 Merge pull request 'fix(ci): install gcloud in build-deps step to avoid apt timeout at publish' (#50) from fix/ci-gcloud-install-order into dev
El SDK CI - dev / build-and-test (push) Successful in 6m36s
2026-05-08 17:38:15 +00:00
will.anderson 253ee2b887 fix(ci): install gcloud in build-deps step to avoid apt timeout at publish
El SDK CI - dev / build-and-test (pull_request) Successful in 3m20s
2026-05-08 12:33:57 -05:00
will.anderson d7540700d4 Merge pull request 'perf(ci): precompile el_runtime.o once for all native test modules' (#49) from fix/native-test-precompile-runtime into dev
El SDK CI - dev / build-and-test (push) Failing after 4m1s
2026-05-08 17:24:10 +00:00
will.anderson 2ed6b26dde Merge pull request 'promote: stage → main (all elb linker fixes + ci-base rebuild)' (#42) from stage into main
El SDK Release / build-and-release (push) Successful in 6m28s
promote: stage → main (all elb linker fixes + ci-base rebuild)
2026-05-07 14:25:37 +00:00
will.anderson d8e9fd12f4 Merge pull request 'promote: dev → stage (all elb linker fixes)' (#41) from dev into stage
El SDK Release / build-and-release (pull_request) Successful in 3m51s
El SDK CI - stage / build-and-test (push) Successful in 4m11s
promote: dev → stage (all elb linker fixes)
2026-05-07 14:20:53 +00:00
will.anderson 8fa9c4ba20 Merge pull request 'promote: dev → stage (elb linker fixes)' (#38) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 1m2s
El SDK CI - stage / build-and-test (push) Successful in 3m56s
promote: dev → stage (elb linker fixes)
2026-05-07 08:11:38 +00:00
will.anderson 9c7bde47dc Merge pull request 'promote: dev → stage (elb gcc fix)' (#35) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 40s
El SDK CI - stage / build-and-test (push) Successful in 3m45s
promote: dev → stage (elb gcc fix)
2026-05-07 08:01:22 +00:00
will.anderson c0553459e1 Merge pull request 'promote: dev → stage (CI rebuild fix + ci-base refresh)' (#32) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 35s
El SDK CI - stage / build-and-test (push) Successful in 3m47s
promote: dev → stage (CI rebuild fix + ci-base refresh)
2026-05-07 07:50:27 +00:00
will.anderson fd208583fe Merge pull request 'promote: dev → stage (elb build fix)' (#28) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m51s
El SDK Release / build-and-release (pull_request) Failing after 38s
promote: dev → stage (elb build fix)
2026-05-07 02:46:27 +00:00
will.anderson 3e29fc43ab Merge pull request 'promote: dev → stage (__http_do_map_to_file)' (#25) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m44s
El SDK Release / build-and-release (pull_request) Failing after 47s
2026-05-07 02:14:30 +00:00
will.anderson 979a5677d5 Merge pull request 'promote: dev → stage (__-prefixed runtime fix)' (#22) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m48s
El SDK Release / build-and-release (pull_request) Failing after 1m4s
2026-05-07 01:48:32 +00:00
will.anderson 17b1aa0736 Merge pull request 'promote: dev → stage (return type fix)' (#19) from dev into stage
El SDK CI - stage / build-and-test (push) Failing after 4m1s
El SDK Release / build-and-release (pull_request) Failing after 42s
2026-05-07 01:12:18 +00:00
will.anderson f0c731d2db Merge pull request 'promote: dev → stage (runtime fix)' (#16) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 3m43s
El SDK Release / build-and-release (pull_request) Failing after 45s
2026-05-07 00:43:52 +00:00
will.anderson e7e0f7d3e5 Merge pull request 'promote: dev → stage' (#12) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 4m3s
El SDK Release / build-and-release (pull_request) Failing after 37s
2026-05-07 00:23:46 +00:00
225 changed files with 107535 additions and 601 deletions
+4 -4
View File
@@ -22,7 +22,10 @@ jobs:
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
> /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
@@ -212,9 +215,6 @@ jobs:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
+3 -3
View File
@@ -1,7 +1,7 @@
// auto-generated by elc --emit-header — do not edit
extern fn sem_get(json: String, key: String) -> String
extern fn generate_frame(frame: Any) -> String
extern fn generate_frame_lang(frame: Any, lang_code: String) -> String
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> Any
extern fn generate_frame(frame: [String]) -> String
extern fn generate_frame_lang(frame: [String], lang_code: String) -> String
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String]
extern fn generate(semantic_form_json: String) -> String
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
+28 -28
View File
@@ -1,22 +1,22 @@
// auto-generated by elc --emit-header - do not edit
extern fn slots_get(slots: Any, key: String) -> String
extern fn slots_set(slots: Any, key: String, val: String) -> Any
extern fn make_slots(k0: String, v0: String) -> Any
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> Any
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> Any
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> Any
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> Any
extern fn rule_id(rule: Any) -> String
extern fn rule_lhs(rule: Any) -> String
extern fn rule_rhs_len(rule: Any) -> Int
extern fn rule_rhs(rule: Any, idx: Int) -> String
extern fn make_rule(id: String, lhs: String, r0: String) -> Any
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> Any
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> Any
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> Any
extern fn build_rules() -> Any
extern fn get_rules() -> Any
extern fn find_rule(rule_id_str: String) -> Any
// auto-generated by elc --emit-header do not edit
extern fn slots_get(slots: [String], key: String) -> String
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
extern fn make_slots(k0: String, v0: String) -> [String]
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
extern fn rule_id(rule: [String]) -> String
extern fn rule_lhs(rule: [String]) -> String
extern fn rule_rhs_len(rule: [String]) -> Int
extern fn rule_rhs(rule: [String], idx: Int) -> String
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
extern fn build_rules() -> [[String]]
extern fn get_rules() -> [[String]]
extern fn find_rule(rule_id_str: String) -> [String]
extern fn make_leaf(label: String, word: String) -> String
extern fn make_node1(label: String, child0: String) -> String
extern fn make_node2(label: String, child0: String, child1: String) -> String
@@ -24,15 +24,15 @@ extern fn make_node3(label: String, child0: String, child1: String, child2: Stri
extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String
extern fn nlg_is_ws(c: String) -> Bool
extern fn skip_ws(s: String, pos: Int) -> Int
extern fn scan_token(s: String, start: Int) -> Any
extern fn scan_token(s: String, start: Int) -> [String]
extern fn render_tree(tree: String) -> String
extern fn gram_word_order(profile: Any) -> String
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: Any) -> String
extern fn gram_build_vp(verb: String, aux: String, profile: Any) -> String
extern fn gram_question_strategy(profile: Any) -> String
extern fn gram_word_order(profile: [String]) -> String
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String
extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String
extern fn gram_question_strategy(profile: [String]) -> String
extern fn is_pronoun(word: String) -> Bool
extern fn build_np(referent: String, slots: Any) -> String
extern fn build_np(referent: String, slots: [String]) -> String
extern fn build_pp(loc: String) -> String
extern fn build_vp_body(slots: Any) -> String
extern fn build_vp_from_slots(slots: Any) -> String
extern fn generate_tree(rule_id_str: String, slots: Any) -> String
extern fn build_vp_body(slots: [String]) -> String
extern fn build_vp_from_slots(slots: [String]) -> String
extern fn generate_tree(rule_id_str: String, slots: [String]) -> String
+46 -46
View File
@@ -1,46 +1,46 @@
// auto-generated by elc --emit-header - do not edit
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> Any
extern fn lang_get(profile: Any, key: String) -> String
extern fn lang_profile_en() -> Any
extern fn lang_profile_ja() -> Any
extern fn lang_profile_ar() -> Any
extern fn lang_profile_zh() -> Any
extern fn lang_profile_de() -> Any
extern fn lang_profile_es() -> Any
extern fn lang_profile_fi() -> Any
extern fn lang_profile_sw() -> Any
extern fn lang_profile_hi() -> Any
extern fn lang_profile_ru() -> Any
extern fn lang_profile_fr() -> Any
extern fn lang_profile_la() -> Any
extern fn lang_profile_he() -> Any
extern fn lang_profile_sa() -> Any
extern fn lang_profile_got() -> Any
extern fn lang_profile_non() -> Any
extern fn lang_profile_enm() -> Any
extern fn lang_profile_pi() -> Any
extern fn lang_profile_grc() -> Any
extern fn lang_profile_ang() -> Any
extern fn lang_profile_fro() -> Any
extern fn lang_profile_goh() -> Any
extern fn lang_profile_sga() -> Any
extern fn lang_profile_txb() -> Any
extern fn lang_profile_peo() -> Any
extern fn lang_profile_akk() -> Any
extern fn lang_profile_uga() -> Any
extern fn lang_profile_egy() -> Any
extern fn lang_profile_sux() -> Any
extern fn lang_profile_gez() -> Any
extern fn lang_profile_cop() -> Any
extern fn lang_from_code(code: String) -> Any
extern fn lang_default() -> Any
extern fn lang_is_isolating(profile: Any) -> Bool
extern fn lang_is_agglutinative(profile: Any) -> Bool
extern fn lang_is_fusional(profile: Any) -> Bool
extern fn lang_is_polysynthetic(profile: Any) -> Bool
extern fn lang_is_rtl(profile: Any) -> Bool
extern fn lang_has_null_subject(profile: Any) -> Bool
extern fn lang_has_case(profile: Any) -> Bool
extern fn lang_has_gender(profile: Any) -> Bool
extern fn lang_word_order(profile: Any) -> String
extern fn lang_code(profile: Any) -> String
// auto-generated by elc --emit-header do not edit
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
extern fn lang_get(profile: [String], key: String) -> String
extern fn lang_profile_en() -> [String]
extern fn lang_profile_ja() -> [String]
extern fn lang_profile_ar() -> [String]
extern fn lang_profile_zh() -> [String]
extern fn lang_profile_de() -> [String]
extern fn lang_profile_es() -> [String]
extern fn lang_profile_fi() -> [String]
extern fn lang_profile_sw() -> [String]
extern fn lang_profile_hi() -> [String]
extern fn lang_profile_ru() -> [String]
extern fn lang_profile_fr() -> [String]
extern fn lang_profile_la() -> [String]
extern fn lang_profile_he() -> [String]
extern fn lang_profile_sa() -> [String]
extern fn lang_profile_got() -> [String]
extern fn lang_profile_non() -> [String]
extern fn lang_profile_enm() -> [String]
extern fn lang_profile_pi() -> [String]
extern fn lang_profile_grc() -> [String]
extern fn lang_profile_ang() -> [String]
extern fn lang_profile_fro() -> [String]
extern fn lang_profile_goh() -> [String]
extern fn lang_profile_sga() -> [String]
extern fn lang_profile_txb() -> [String]
extern fn lang_profile_peo() -> [String]
extern fn lang_profile_akk() -> [String]
extern fn lang_profile_uga() -> [String]
extern fn lang_profile_egy() -> [String]
extern fn lang_profile_sux() -> [String]
extern fn lang_profile_gez() -> [String]
extern fn lang_profile_cop() -> [String]
extern fn lang_from_code(code: String) -> [String]
extern fn lang_default() -> [String]
extern fn lang_is_isolating(profile: [String]) -> Bool
extern fn lang_is_agglutinative(profile: [String]) -> Bool
extern fn lang_is_fusional(profile: [String]) -> Bool
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
extern fn lang_is_rtl(profile: [String]) -> Bool
extern fn lang_has_null_subject(profile: [String]) -> Bool
extern fn lang_has_case(profile: [String]) -> Bool
extern fn lang_has_gender(profile: [String]) -> Bool
extern fn lang_word_order(profile: [String]) -> String
extern fn lang_code(profile: [String]) -> String
+1
View File
@@ -56,6 +56,7 @@
// String helpers
import "morphology.el"
fn akk_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn akk_str_ends(s: String, suf: String) -> Bool
extern fn akk_str_len(s: String) -> Int
extern fn akk_str_drop_last(s: String, n: Int) -> String
+1
View File
@@ -36,6 +36,7 @@
// String helpers
import "morphology.el"
fn ang_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn ang_str_ends(s: String, suf: String) -> Bool
extern fn ang_str_drop_last(s: String, n: Int) -> String
extern fn ang_str_last_char(s: String) -> String
+1
View File
@@ -21,6 +21,7 @@
// String helpers
import "morphology.el"
fn ar_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn ar_str_ends(s: String, suf: String) -> Bool
extern fn ar_str_len(s: String) -> Int
extern fn ar_str_drop_last(s: String, n: Int) -> String
+1
View File
@@ -54,6 +54,7 @@
// String helpers
import "morphology.el"
fn cop_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn cop_str_ends(s: String, suf: String) -> Bool
extern fn cop_str_len(s: String) -> Int
extern fn cop_drop(s: String, n: Int) -> String
+1
View File
@@ -26,6 +26,7 @@
// Dat: dem der dem den
// Gen: des der des der
import "morphology.el"
fn de_article_def(gender: String, gram_case: String, number: String) -> String {
if str_eq(number, "pl") {
if str_eq(gram_case, "nom") { return "die" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn de_article_def(gender: String, gram_case: String, number: String) -> String
extern fn de_article_indef(gender: String, gram_case: String, number: String) -> String
extern fn de_article(gender: String, gram_case: String, number: String, definite: String) -> String
+1
View File
@@ -52,6 +52,7 @@
// String helpers
import "morphology.el"
fn egy_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn egy_str_ends(s: String, suf: String) -> Bool
extern fn egy_str_len(s: String) -> Int
extern fn egy_drop(s: String, n: Int) -> String
+1
View File
@@ -31,6 +31,7 @@
// String helpers
import "morphology.el"
fn enm_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn enm_str_ends(s: String, suf: String) -> Bool
extern fn enm_drop(s: String, n: Int) -> String
extern fn enm_first_char(s: String) -> String
+1
View File
@@ -12,6 +12,7 @@
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn es_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn es_str_ends(s: String, suf: String) -> Bool
extern fn es_str_drop_last(s: String, n: Int) -> String
extern fn es_str_last_char(s: String) -> String
+1
View File
@@ -25,6 +25,7 @@
// If only neutral vowels are found, default to "front" (the conservative choice
// for borrowed words and those without clear back vowels).
import "morphology.el"
fn fi_harmony(word: String) -> String {
let n: Int = str_len(word)
let i: Int = n - 1
+3 -3
View File
@@ -1,11 +1,11 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn fi_harmony(word: String) -> String
extern fn fi_suffix(base: String, harmony: String) -> String
extern fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String
extern fn fi_str_last_char(s: String) -> String
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
extern fn fi_verb_stem(dict_form: String) -> String
extern fn fi_irregular_verb(dict_form: String) -> Any
extern fn fi_irregular_verb(dict_form: String) -> [String]
extern fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String
extern fn fi_past_stem(stem: String) -> String
extern fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String
@@ -14,4 +14,4 @@ extern fn fi_negative(verb: String, person: String, number: String) -> String
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fi_question_suffix(harmony: String) -> String
extern fn fi_make_question(verb_form: String, harmony: String) -> String
extern fn fi_full_paradigm(noun: String) -> Any
extern fn fi_full_paradigm(noun: String) -> [String]
+1
View File
@@ -19,6 +19,7 @@
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn fr_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn fr_str_ends(s: String, suf: String) -> Bool
extern fn fr_str_drop_last(s: String, n: Int) -> String
extern fn fr_str_last_char(s: String) -> String
+1
View File
@@ -53,6 +53,7 @@
// String helpers
import "morphology.el"
fn fro_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn fro_str_ends(s: String, suf: String) -> Bool
extern fn fro_drop(s: String, n: Int) -> String
extern fn fro_slot(person: String, number: String) -> Int
+1
View File
@@ -64,6 +64,7 @@
// String helpers
import "morphology.el"
fn gez_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn gez_str_ends(s: String, suf: String) -> Bool
extern fn gez_str_len(s: String) -> Int
extern fn gez_str_drop_last(s: String, n: Int) -> String
+1
View File
@@ -48,6 +48,7 @@
// String helpers
import "morphology.el"
fn goh_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn goh_str_ends(s: String, suf: String) -> Bool
extern fn goh_drop(s: String, n: Int) -> String
extern fn goh_slot(person: String, number: String) -> Int
+1
View File
@@ -49,6 +49,7 @@
// String helpers
import "morphology.el"
fn got_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn got_str_ends(s: String, suf: String) -> Bool
extern fn got_str_drop_last(s: String, n: Int) -> String
extern fn got_slot(person: String, number: String) -> Int
+1
View File
@@ -31,6 +31,7 @@
// String helpers
import "morphology.el"
fn grc_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn grc_str_ends(s: String, suf: String) -> Bool
extern fn grc_str_drop_last(s: String, n: Int) -> String
extern fn grc_str_last_char(s: String) -> String
+1
View File
@@ -51,6 +51,7 @@
// String helpers
import "morphology.el"
fn he_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn he_str_ends(s: String, suf: String) -> Bool
extern fn he_str_len(s: String) -> Int
extern fn he_str_drop_last(s: String, n: Int) -> String
+1
View File
@@ -24,6 +24,7 @@
// String helpers
import "morphology.el"
fn hi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn hi_str_ends(s: String, suf: String) -> Bool
extern fn hi_str_drop_last(s: String, n: Int) -> String
extern fn hi_str_last_char(s: String) -> String
+1
View File
@@ -23,6 +23,7 @@
// Note: this is a heuristic classifier for romanized input. For production use
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
import "morphology.el"
fn ja_verb_group(dict_form: String) -> String {
// Irregular verbs (exact match on dictionary form)
if str_eq(dict_form, "する") { return "irregular" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn ja_verb_group(dict_form: String) -> String
extern fn ja_ichidan_stem(dict_form: String) -> String
extern fn ja_godan_stem_change(dict_form: String, row: String) -> String
+1
View File
@@ -25,6 +25,7 @@
// String helpers
import "morphology.el"
fn la_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn la_str_ends(s: String, suf: String) -> Bool
extern fn la_str_drop_last(s: String, n: Int) -> String
extern fn la_str_last_char(s: String) -> String
+1
View File
@@ -27,6 +27,7 @@
// String helpers
import "morphology.el"
fn non_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn non_str_ends(s: String, suf: String) -> Bool
extern fn non_drop(s: String, n: Int) -> String
extern fn non_last(s: String) -> String
+1
View File
@@ -31,6 +31,7 @@
// String helpers
import "morphology.el"
fn peo_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn peo_drop(s: String, n: Int) -> String
extern fn peo_ends(s: String, suf: String) -> Bool
extern fn peo_slot(person: String, number: String) -> Int
+1
View File
@@ -30,6 +30,7 @@
// String helpers
import "morphology.el"
fn pi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn pi_str_ends(s: String, suf: String) -> Bool
extern fn pi_drop(s: String, n: Int) -> String
extern fn pi_last_char(s: String) -> String
+1
View File
@@ -35,6 +35,7 @@
// The heuristic returns the most probable gender. Caller should override
// for known exceptions (путь, рубль are masc despite ).
import "morphology.el"
fn ru_gender(noun: String) -> String {
let n: Int = str_len(noun)
if n == 0 { return "m" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn ru_gender(noun: String) -> String
extern fn ru_stem_type(noun: String, gender: String) -> String
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
+1
View File
@@ -42,6 +42,7 @@
// String helpers
import "morphology.el"
fn sa_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn sa_str_ends(s: String, suf: String) -> Bool
extern fn sa_str_drop_last(s: String, n: Int) -> String
extern fn sa_slot(person: String, number: String) -> Int
+1
View File
@@ -31,6 +31,7 @@
// String helpers
import "morphology.el"
fn sga_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn sga_drop(s: String, n: Int) -> String
extern fn sga_first(s: String) -> String
extern fn sga_rest(s: String) -> String
+1
View File
@@ -53,6 +53,7 @@
// String helpers
import "morphology.el"
fn sux_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn sux_str_ends(s: String, suf: String) -> Bool
extern fn sux_str_drop_last(s: String, n: Int) -> String
extern fn sux_str_last_char(s: String) -> String
+1
View File
@@ -24,6 +24,7 @@
// String helpers
import "morphology.el"
fn sw_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn sw_str_ends(s: String, suf: String) -> Bool
extern fn sw_str_drop_last(s: String, n: Int) -> String
extern fn sw_str_first_char(s: String) -> String
+1
View File
@@ -30,6 +30,7 @@
// String helpers
import "morphology.el"
fn txb_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn txb_drop(s: String, n: Int) -> String
extern fn txb_ends(s: String, suf: String) -> Bool
extern fn txb_slot(person: String, number: String) -> Int
+1
View File
@@ -48,6 +48,7 @@
// String helpers
import "morphology.el"
fn uga_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn uga_str_ends(s: String, suf: String) -> Bool
extern fn uga_str_len(s: String) -> Int
extern fn uga_str_drop_last(s: String, n: Int) -> String
+11
View File
@@ -33,6 +33,17 @@
// String helpers
import "language-profile.el"
import "morphology-es.el"
import "morphology-fr.el"
import "morphology-de.el"
import "morphology-ru.el"
import "morphology-fi.el"
import "morphology-ar.el"
import "morphology-hi.el"
import "morphology-sw.el"
import "morphology-la.el"
import "morphology-ja.el"
fn str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
+5 -5
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn str_ends(s: String, suf: String) -> Bool
extern fn str_last_char(s: String) -> String
extern fn str_last2(s: String) -> String
@@ -8,7 +8,7 @@ extern fn is_vowel(c: String) -> Bool
extern fn morph_apply_suffix(base: String, suffix: String) -> String
extern fn en_irregular_plural(word: String) -> String
extern fn en_irregular_singular(word: String) -> String
extern fn en_irregular_verb(base: String) -> Any
extern fn en_irregular_verb(base: String) -> [String]
extern fn en_verb_3sg(base: String) -> String
extern fn en_should_double_final(base: String) -> Bool
extern fn en_verb_past(base: String) -> String
@@ -16,10 +16,10 @@ extern fn en_verb_gerund(base: String) -> String
extern fn en_pluralize_regular(singular: String) -> String
extern fn en_verb_form(base: String, tense: String, person: String, number: String) -> String
extern fn agree_determiner(det: String, noun: String) -> String
extern fn morph_pluralize(noun: String, profile: Any) -> String
extern fn morph_pluralize(noun: String, profile: [String]) -> String
extern fn morph_map_canonical(verb: String, code: String) -> String
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: Any) -> String
extern fn morph_inflect(word: String, features: String, profile: Any) -> String
extern fn morph_conjugate(verb: String, tense: String, person: String, number: String, profile: [String]) -> String
extern fn morph_inflect(word: String, features: String, profile: [String]) -> String
extern fn pluralize(singular: String) -> String
extern fn singularize(plural: String) -> String
extern fn verb_form(base: String, tense: String, person: String, number: String) -> String
+5 -5
View File
@@ -1,10 +1,10 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn agent_person(agent: String) -> String
extern fn agent_number(agent: String) -> String
extern fn realize_np(referent: String, number: String) -> String
extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: Any) -> Any
extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: Any) -> String
extern fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String]
extern fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String
extern fn capitalize_first(s: String) -> String
extern fn add_punct(s: String, intent: String) -> String
extern fn realize_lang(form: Any, profile: Any) -> String
extern fn realize(form: Any) -> String
extern fn realize_lang(form: [String], profile: [String]) -> String
extern fn realize(form: [String]) -> String
+15 -15
View File
@@ -1,18 +1,18 @@
// auto-generated by elc --emit-header - do not edit
extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> Any
extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> Any
extern fn sem_frame_simple(intent: String, subject: String) -> Any
extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> Any
extern fn sem_intent(frame: Any) -> String
extern fn sem_subject(frame: Any) -> String
extern fn sem_object(frame: Any) -> String
extern fn sem_modifiers(frame: Any) -> String
extern fn sem_lang(frame: Any) -> String
// auto-generated by elc --emit-header do not edit
extern fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String]
extern fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> [String]
extern fn sem_frame_simple(intent: String, subject: String) -> [String]
extern fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String]
extern fn sem_intent(frame: [String]) -> String
extern fn sem_subject(frame: [String]) -> String
extern fn sem_object(frame: [String]) -> String
extern fn sem_modifiers(frame: [String]) -> String
extern fn sem_lang(frame: [String]) -> String
extern fn sem_first_modifier(mods: String) -> String
extern fn sem_intent_to_realize(intent: String) -> String
extern fn sem_to_spec(frame: Any) -> Any
extern fn sem_to_spec_full(frame: Any, verb: String, tense: String, aspect: String) -> Any
extern fn sem_to_spec(frame: [String]) -> [String]
extern fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String]
extern fn sem_realize_greet(subject: String) -> String
extern fn sem_realize(frame: Any) -> String
extern fn sem_realize_full(frame: Any, verb: String, tense: String, aspect: String) -> String
extern fn sem_realize_lang(frame: Any, lang_code: String) -> String
extern fn sem_realize(frame: [String]) -> String
extern fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String
extern fn sem_realize_lang(frame: [String], lang_code: String) -> String
+19 -19
View File
@@ -1,20 +1,20 @@
// auto-generated by elc --emit-header - do not edit
extern fn lex_word(entry: Any) -> String
extern fn lex_pos(entry: Any) -> String
extern fn lex_form(entry: Any, idx: Int) -> String
extern fn lex_class(entry: Any) -> String
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> Any
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> Any
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> Any
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> Any
extern fn build_vocab() -> Any
extern fn get_vocab() -> Any
extern fn vocab_lookup(word: String, lang_code: String) -> Any
extern fn vocab_lookup_en(word: String) -> Any
// auto-generated by elc --emit-header do not edit
extern fn lex_word(entry: [String]) -> String
extern fn lex_pos(entry: [String]) -> String
extern fn lex_form(entry: [String], idx: Int) -> String
extern fn lex_class(entry: [String]) -> String
extern fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> [String]
extern fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> [String]
extern fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> [String]
extern fn make_entry1(word: String, pos: String, f0: String, cls: String) -> [String]
extern fn build_vocab() -> [[String]]
extern fn get_vocab() -> [[String]]
extern fn vocab_lookup(word: String, lang_code: String) -> [String]
extern fn vocab_lookup_en(word: String) -> [String]
extern fn vocab_synonym(word: String, lang_register: String, lang_code: String) -> String
extern fn vocab_by_pos(pos: String) -> Any
extern fn vocab_by_class(cls: String) -> Any
extern fn entry_found(entry: Any) -> Bool
extern fn entry_word(entry: Any) -> String
extern fn entry_pos(entry: Any) -> String
extern fn entry_form(entry: Any, n: Int) -> String
extern fn vocab_by_pos(pos: String) -> [[String]]
extern fn vocab_by_class(cls: String) -> [[String]]
extern fn entry_found(entry: [String]) -> Bool
extern fn entry_word(entry: [String]) -> String
extern fn entry_pos(entry: [String]) -> String
extern fn entry_form(entry: [String], n: Int) -> String
Vendored Executable
BIN
View File
Binary file not shown.
+260 -75
View File
@@ -10,6 +10,9 @@ el_val_t query_param(el_val_t path, el_val_t key);
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val);
el_val_t extract_id(el_val_t path, el_val_t prefix);
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body);
el_val_t persist_canonical(void);
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body);
@@ -17,19 +20,29 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body);
el_val_t check_auth_ok(el_val_t method, el_val_t body);
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
el_val_t bind_raw;
el_val_t bind_str;
el_val_t port;
el_val_t data_dir_raw;
el_val_t data_dir;
el_val_t snapshot_path;
el_val_t boot_snap;
el_val_t parse_port(el_val_t bind) {
el_val_t colon = str_index_of(bind, EL_STR(":"));
@@ -108,17 +121,40 @@ el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
return 0;
}
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body) {
return engram_act_stats_json();
return 0;
}
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body) {
return engram_text_health_json();
return 0;
}
el_val_t persist_canonical(void) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_1 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_1 = (EL_STR("/tmp/engram")); } else { _if_result_1 = (dir_raw); } _if_result_1; });
return engram_save(el_str_concat(dir, EL_STR("/snapshot.json")));
return 0;
}
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
el_val_t node_type = json_get_string(body, EL_STR("node_type"));
if (str_eq(node_type, EL_STR(""))) {
node_type = EL_STR("Memory");
}
el_val_t salience = json_get_float(body, EL_STR("salience"));
if (str_eq(salience, el_from_float(0.0))) {
salience = el_from_float(0.5);
}
el_val_t id = engram_node(content, node_type, salience);
el_val_t nt_raw = json_get_string(body, EL_STR("node_type"));
el_val_t node_type = ({ el_val_t _if_result_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
el_val_t sal_present = json_get_raw(body, EL_STR("salience"));
el_val_t salience = ({ el_val_t _if_result_3 = 0; if (str_eq(sal_present, EL_STR(""))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (json_get_float(body, EL_STR("salience"))); } _if_result_3; });
el_val_t label_raw = json_get_string(body, EL_STR("label"));
el_val_t label = ({ el_val_t _if_result_4 = 0; if (str_eq(label_raw, EL_STR(""))) { _if_result_4 = (content); } else { _if_result_4 = (label_raw); } _if_result_4; });
el_val_t imp_present = json_get_raw(body, EL_STR("importance"));
el_val_t importance = ({ el_val_t _if_result_5 = 0; if (str_eq(imp_present, EL_STR(""))) { _if_result_5 = (el_from_float(0.5)); } else { _if_result_5 = (json_get_float(body, EL_STR("importance"))); } _if_result_5; });
el_val_t conf_present = json_get_raw(body, EL_STR("confidence"));
el_val_t confidence = ({ el_val_t _if_result_6 = 0; if (str_eq(conf_present, EL_STR(""))) { _if_result_6 = (el_from_float(1.0)); } else { _if_result_6 = (json_get_float(body, EL_STR("confidence"))); } _if_result_6; });
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
el_val_t tier = ({ el_val_t _if_result_7 = 0; if (str_eq(tier_raw, EL_STR(""))) { _if_result_7 = (EL_STR("Working")); } else { _if_result_7 = (tier_raw); } _if_result_7; });
el_val_t tags = json_get_string(body, EL_STR("tags"));
el_val_t id = engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
return 0;
}
@@ -144,11 +180,9 @@ el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
}
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_8 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_8 = (EL_STR("/tmp/engram")); } else { _if_result_8 = (dir_raw); } _if_result_8; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
@@ -163,36 +197,22 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
}
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = EL_STR("");
if (str_eq(method, EL_STR("GET"))) {
q = query_param(path, EL_STR("q"));
} else {
q = json_get_string(body, EL_STR("query"));
}
el_val_t limit = query_int(path, EL_STR("limit"), 20);
if (limit == 0) {
limit = json_get_int(body, EL_STR("limit"));
}
if (limit == 0) {
limit = 20;
}
el_val_t q = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_param(path, EL_STR("q"))); } else { _if_result_9 = (json_get_string(body, EL_STR("query"))); } _if_result_9; });
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
el_val_t lim_either = ({ el_val_t _if_result_10 = 0; if ((lim_url > 0)) { _if_result_10 = (lim_url); } else { _if_result_10 = (lim_body); } _if_result_10; });
el_val_t limit = ({ el_val_t _if_result_11 = 0; if ((lim_either > 0)) { _if_result_11 = (lim_either); } else { _if_result_11 = (20); } _if_result_11; });
return engram_search_json(q, limit);
return 0;
}
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = EL_STR("");
el_val_t depth = 3;
if (str_eq(method, EL_STR("GET"))) {
q = query_param(path, EL_STR("q"));
depth = query_int(path, EL_STR("depth"), 3);
} else {
q = json_get_string(body, EL_STR("query"));
el_val_t bd = json_get_int(body, EL_STR("depth"));
if (bd > 0) {
depth = bd;
}
el_val_t q = ({ el_val_t _if_result_12 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_12 = (query_param(path, EL_STR("q"))); } else { _if_result_12 = (json_get_string(body, EL_STR("query"))); } _if_result_12; });
if (str_eq(q, EL_STR(""))) {
return err_json(EL_STR("missing query"));
}
el_val_t d_raw = ({ el_val_t _if_result_13 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_13 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_13 = (json_get_int(body, EL_STR("depth"))); } _if_result_13; });
el_val_t depth = ({ el_val_t _if_result_14 = 0; if ((d_raw > 0)) { _if_result_14 = (d_raw); } else { _if_result_14 = (3); } _if_result_14; });
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
return 0;
}
@@ -200,19 +220,51 @@ el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
el_val_t relation = json_get_string(body, EL_STR("relation"));
if (str_eq(relation, EL_STR(""))) {
relation = EL_STR("associates");
}
el_val_t weight = json_get_float(body, EL_STR("weight"));
if (str_eq(weight, el_from_float(0.0))) {
weight = el_from_float(0.5);
}
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_15 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_15 = (EL_STR("associates")); } else { _if_result_15 = (rel_raw); } _if_result_15; });
el_val_t w_present = json_get_raw(body, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_16 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_16 = (el_from_float(0.5)); } else { _if_result_16 = (json_get_float(body, EL_STR("weight"))); } _if_result_16; });
engram_connect(from_id, to_id, weight, relation);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}"));
return 0;
}
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body) {
el_val_t arr = json_get_raw(body, EL_STR("edges"));
if (str_eq(arr, EL_STR(""))) {
return err_json(EL_STR("missing edges array"));
}
el_val_t n = json_array_len(arr);
if (n == 0) {
return EL_STR("{\"ok\":true,\"accepted\":0,\"skipped\":0}");
}
el_val_t i = 0;
el_val_t accepted = 0;
el_val_t skipped = 0;
while (i < n) {
el_val_t item = json_array_get(arr, i);
el_val_t from_id = json_get_string(item, EL_STR("from_id"));
el_val_t to_id = json_get_string(item, EL_STR("to_id"));
if (str_eq(from_id, EL_STR("")) || str_eq(to_id, EL_STR(""))) {
skipped = (skipped + 1);
} else {
el_val_t rel_raw = json_get_string(item, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_17 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_17 = (EL_STR("associates")); } else { _if_result_17 = (rel_raw); } _if_result_17; });
el_val_t w_present = json_get_raw(item, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_18 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_18 = (el_from_float(0.5)); } else { _if_result_18 = (json_get_float(item, EL_STR("weight"))); } _if_result_18; });
engram_connect(from_id, to_id, weight, relation);
accepted = (accepted + 1);
}
i = (i + 1);
}
if (accepted > 0) {
el_val_t saved = persist_canonical();
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"accepted\":"), int_to_str(accepted)), EL_STR(",\"skipped\":")), int_to_str(skipped)), EL_STR("}"));
return 0;
}
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body) {
el_val_t id = extract_id(path, EL_STR("/api/neighbors/"));
if (str_eq(id, EL_STR(""))) {
@@ -229,6 +281,7 @@ el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) {
return err_json(EL_STR("missing node_id"));
}
engram_strengthen(id);
el_val_t saved = persist_canonical();
return ok_json();
return 0;
}
@@ -239,40 +292,140 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
return err_json(EL_STR("missing id"));
}
engram_forget(id);
el_val_t saved = persist_canonical();
return ok_json();
return 0;
}
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
p = el_str_concat(dir, EL_STR("/snapshot.json"));
}
engram_save(p);
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_19 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_19 = (EL_STR("/tmp/engram")); } else { _if_result_19 = (dir_raw); } _if_result_19; });
el_val_t p = ({ el_val_t _if_result_20 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_20 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_20 = (p_raw); } _if_result_20; });
el_val_t sv = engram_save(p);
el_val_t sv_ok = ({ el_val_t _if_result_21 = 0; if ((sv == 0)) { _if_result_21 = (EL_STR("false")); } else { _if_result_21 = (EL_STR("true")); } _if_result_21; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":"), sv_ok), EL_STR(",\"path\":\"")), p), EL_STR("\",\"node_count\":")), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
return 0;
}
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
p = el_str_concat(dir, EL_STR("/snapshot.json"));
}
engram_load(p);
return ok_json();
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_22 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_22 = (EL_STR("/tmp/engram")); } else { _if_result_22 = (dir_raw); } _if_result_22; });
el_val_t p = ({ el_val_t _if_result_23 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_23 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_23 = (p_raw); } _if_result_23; });
el_val_t ld = engram_load(p);
el_val_t ld_ok = ({ el_val_t _if_result_24 = 0; if ((ld == 0)) { _if_result_24 = (EL_STR("false")); } else { _if_result_24 = (EL_STR("true")); } _if_result_24; });
el_val_t nc_after = engram_node_count();
el_val_t hollow = ({ el_val_t _if_result_25 = 0; if ((nc_after == 0)) { _if_result_25 = (EL_STR("true")); } else { _if_result_25 = (EL_STR("false")); } _if_result_25; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":"), ld_ok), EL_STR(",\"path\":\"")), p), EL_STR("\",\"node_count\":")), int_to_str(nc_after)), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR(",\"hollow\":")), hollow), EL_STR("}"));
return 0;
}
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}");
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\",\"node_count\":"), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
return 0;
}
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body) {
el_val_t n = query_int(path, EL_STR("n"), 32);
el_val_t result = engram_embed_backfill(n);
el_val_t done = json_get_float(result, EL_STR("embedded"));
if (done > el_from_float(0.0)) {
el_val_t saved = persist_canonical();
}
return result;
return 0;
}
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_26 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_26 = (EL_STR("/tmp/engram")); } else { _if_result_26 = (dir_raw); } _if_result_26; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
return err_json(EL_STR("sync export failed: snapshot unreadable"));
}
return snap;
return 0;
}
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
return err_json(EL_STR("path is required"));
}
if (str_eq(fs_read(p), EL_STR(""))) {
return err_json(EL_STR("file missing or empty"));
}
el_val_t before_n = engram_node_count();
el_val_t before_e = engram_edge_count();
engram_load_merge(p);
el_val_t added_n = (engram_node_count() - before_n);
el_val_t added_e = (engram_edge_count() - before_e);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"nodes_added\":"), int_to_str(added_n)), EL_STR(",\"edges_added\":")), int_to_str(added_e)), EL_STR(",\"node_count\":")), int_to_str(engram_node_count())), EL_STR("}"));
return 0;
}
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
if (str_eq(content, EL_STR(""))) {
return err_json(EL_STR("missing content"));
}
el_val_t sal = el_from_float(0.3);
el_val_t imp = el_from_float(0.3);
el_val_t conf = el_from_float(0.8);
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), sal, imp, conf, EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]"));
el_val_t ret_raw = env(EL_STR("ENGRAM_ISE_RETENTION_MS"));
el_val_t ret_ms = ({ el_val_t _if_result_27 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_27 = (172800000); } else { _if_result_27 = (str_to_int(ret_raw)); } _if_result_27; });
el_val_t pruned = engram_prune_telemetry(ret_ms);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"pruned\":")), int_to_str(pruned)), EL_STR("}"));
return 0;
}
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
if (str_eq(content, EL_STR(""))) {
return err_json(EL_STR("missing content"));
}
el_val_t title = json_get_string(body, EL_STR("title"));
el_val_t label = ({ el_val_t _if_result_28 = 0; if (str_eq(title, EL_STR(""))) { _if_result_28 = (str_slice(content, 0, 60)); } else { _if_result_28 = (title); } _if_result_28; });
el_val_t category_raw = json_get_string(body, EL_STR("category"));
el_val_t category = ({ el_val_t _if_result_29 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_29 = (EL_STR("other")); } else { _if_result_29 = (category_raw); } _if_result_29; });
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
el_val_t ktier = ({ el_val_t _if_result_30 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_30 = (EL_STR("note")); } else { _if_result_30 = (ktier_raw); } _if_result_30; });
el_val_t project = json_get_string(body, EL_STR("project"));
el_val_t tags_raw = json_get_raw(body, EL_STR("tags"));
el_val_t tags_base = ({ el_val_t _if_result_31 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_31 = (EL_STR("[]")); } else { _if_result_31 = (tags_raw); } _if_result_31; });
el_val_t base_len = str_len(tags_base);
el_val_t head = str_slice(tags_base, 0, (base_len - 1));
el_val_t sep = ({ el_val_t _if_result_32 = 0; if (str_eq(head, EL_STR("["))) { _if_result_32 = (EL_STR("")); } else { _if_result_32 = (EL_STR(",")); } _if_result_32; });
el_val_t safe_cat = str_replace(category, EL_STR("\""), EL_STR("'"));
el_val_t safe_tier = str_replace(ktier, EL_STR("\""), EL_STR("'"));
el_val_t safe_proj = str_replace(project, EL_STR("\""), EL_STR("'"));
el_val_t proj_tag = ({ el_val_t _if_result_33 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_33 = (EL_STR("")); } else { _if_result_33 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_33; });
el_val_t tags = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(head, sep), EL_STR("\"category:")), safe_cat), EL_STR("\",\"tier:")), safe_tier), EL_STR("\"")), proj_tag), EL_STR("]"));
el_val_t sal = el_from_float(0.5);
el_val_t imp = el_from_float(0.5);
el_val_t conf = el_from_float(0.9);
el_val_t id = engram_node_full(content, EL_STR("Knowledge"), label, sal, imp, conf, EL_STR("Semantic"), tags);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
return 0;
}
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body) {
el_val_t a = query_param(path, EL_STR("a"));
el_val_t b = query_param(path, EL_STR("b"));
if (str_eq(a, EL_STR(""))) {
return err_json(EL_STR("missing a"));
}
if (str_eq(b, EL_STR(""))) {
return err_json(EL_STR("missing b"));
}
el_val_t sim = engram_cosine_sim(a, b);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"a\":\""), a), EL_STR("\",\"b\":\"")), b), EL_STR("\",\"cosine\":")), float_to_str(sim)), EL_STR("}"));
return 0;
}
@@ -299,12 +452,24 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
return route_health(method, path, body);
}
}
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/state-events"))) {
return route_emit_ise(method, path, body);
}
if (!check_auth_ok(method, body)) {
return err_json(EL_STR("unauthorized"));
}
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/knowledge/capture"))) {
return route_capture_knowledge(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) {
return route_stats(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/act-stats")) || str_eq(clean, EL_STR("/act-stats")))) {
return route_act_stats(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/text-health")) || str_eq(clean, EL_STR("/text-health")))) {
return route_text_health(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) {
return route_create_node(method, path, body);
}
@@ -323,6 +488,9 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) {
return route_create_edge(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges/batch")) || str_eq(clean, EL_STR("/edges/batch")))) {
return route_create_edges_batch(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/neighbors/"))) {
return route_neighbors(method, path, body);
}
@@ -347,23 +515,40 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
return route_load(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load-merge")) || str_eq(clean, EL_STR("/load-merge")))) {
return route_load_merge(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
return route_sync(method, path, body);
}
if (str_eq(clean, EL_STR("/api/embed-backfill"))) {
return route_embed_backfill(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/similarity"))) {
return route_similarity(method, path, body);
}
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
return 0;
}
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
bind_str = env(EL_STR("ENGRAM_BIND"));
if (str_eq(bind_str, EL_STR(""))) {
bind_str = EL_STR(":8742");
}
bind_raw = env(EL_STR("ENGRAM_BIND"));
bind_str = ({ el_val_t _if_result_34 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_34 = (EL_STR(":8742")); } else { _if_result_34 = (bind_raw); } _if_result_34; });
port = parse_port(bind_str);
data_dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(data_dir, EL_STR(""))) {
data_dir = EL_STR("/tmp/engram");
}
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
data_dir = ({ el_val_t _if_result_35 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_35 = (EL_STR("/tmp/engram")); } else { _if_result_35 = (data_dir_raw); } _if_result_35; });
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
engram_load(snapshot_path);
boot_snap = fs_read(snapshot_path);
if (!str_eq(boot_snap, EL_STR(""))) {
if (engram_node_count() == 0) {
println(EL_STR("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes \xe2\x80\x94 preserving copy at snapshot.failed-load.json"));
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.failed-load.json")), boot_snap);
} else {
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.boot-backup.json")), boot_snap);
}
}
println(EL_STR("[engram] runtime-native graph engine"));
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
@@ -0,0 +1,605 @@
# Cognitive Architecture — Design Doc
**The buildable form of the "one operation" theory of cognition.**
Status: DESIGN. Nothing here is built yet except where explicitly marked
"EXISTS" against a cited C symbol. A build agent executes from this doc.
Offline design only — this pass changes no code.
Source of theory: Neuron memory `bdc8a488-146d-4ccb-a5c8-d8c0a008534e`.
Source of existing engram substrate (cited throughout): the runtime on branch
`feat/self-reification-20260814`
`lang/runtime/engram_reason.{c,h}`, `engram_verify.{c,h}`,
`engram_geometry.{c,h}`, `engram_store.{c,h}`, plus the reification beat and the
RAM activation graph compiled into `~/.neuron/bin/engram`.
---
## 0. The claim, stated plainly
Cognition is **one operation**, not eight. The named faculties —
deduce / abduce / analogy / induce / causal / plan / predict / perspective —
are human *labels* on regions of a single operation's steering space. They are
not separately invoked and not separately implemented. The operation is:
> **think** = a directed traversal of the geometry from an *anchor*, steered by
> a *prior*, whose output is a **gradient** (a distribution / direction over the
> geometry), never a point. Collapse-to-a-point happens only at expression.
Three things follow, and they are the whole design:
1. **The operator collapse is already half-written in C.** The five reasoning
operators in `engram_reason.c` already compose over *one* shared primitive —
`engram_reason_point_fit` — plus a small geo-algebra
(combine / subtract / analogy-rotate / distance). The verifier
(`engram_verify.c`) is built on the same `point_fit`. What is missing is not
the primitive; it is (a) making the *prior* a first-class learnable object
instead of a hard-coded parameter, and (b) closing the learning loop.
2. **Grounding = learning = the same loop.** "Getting better" at any faculty is
not changing the operation. It is *calibrating the steering-prior against
outcomes*. Code freezes; priors grow. The correspondence-check that today
lives offline (Python, the grounding-floor + differential-drop governor, "#43")
must move **into the geometry, reflexive** — think scoring its own gradient
against outcome and refining the prior on the error. That reflexive
correspondence-loop *is* the learning engine and is the core unbuilt thing.
3. **The ungrounded is primary.** The engram *holds* anything unconditionally.
Grounding is a *relation* (an edge, grounded-for-whom), not a gate. The
honesty floor applies only to **assertion**. A fully-grounded mind is dead;
the ungrounded is both the fuel (raw material for grounding) and the pull
(curiosity = leaning toward one's own ungrounded regions).
Everything below makes these concrete and buildable, and defines what
"completion" means, staged so the first milestone is a real end-to-end slice.
---
## 1. THE ONE OPERATION — `think`
### 1.1 Signature
```
think(anchor, prior, aperture?) -> gradient
```
- **anchor** — a location to traverse *from*. Either a node id (re-origin on that
node's descriptor) or a raw point `x ∈ R^dim` (a query embedding). The anchor
fixes the frame; every read is *from a vantage*, never view-from-nowhere.
- **prior** — a learnable bias/direction over the geometry that *steers* the
traversal (§2). A prior is a first-class stored object, not a call argument
baked into C.
- **aperture** — optional read-width / veil / field-selector (§3). Absent =
self-mode full aperture.
- **gradient** — the output. A `GeoGradient`: a direction + a spread over the
geometry, *plus* the read neighborhood it was computed against. Not a point.
A spiked gradient = "exact" (deduction); a spread gradient = "fuzzy"
(prediction). The gradient is *also the next steering direction* — cognition
is a flow down a prior-shaped landscape, closed-loop.
```c
/* NEW. The output type. */
typedef struct {
int dim;
float* direction; /* unit steering vector in the anchor's frame */
double spread; /* 0 = spiked/exact ... large = diffuse/fuzzy */
double confidence; /* calibrated, from the prior's track record */
/* the read it was computed over (borrowed from the vantage-read) */
const char* anchor_id;
int n_support; /* neighborhood members that shaped it */
/* provenance for the reflexive loop (§4) */
const char* prior_id; /* which prior steered this */
} GeoGradient;
```
### 1.2 Semantics
`think` is a fixed, frozen procedure over three steps:
1. **Re-origin** on `anchor` → a centered `GeoDescriptor` for its
salience/recency-weighted neighborhood (the vantage-read, §3).
*EXISTS as substrate:* descriptor construction + the persisted reified
neighborhoods (`engram_geo_reify_lookup`, `GeoNeighborhood`) and the
centered-frame machinery (`GeoDescriptor.global_mean`,
`engram_geo_mean_*`).
2. **Fit under the prior** — evaluate the anchor's residual against the local
manifold *warped by the prior*. This is `engram_reason_point_fit` with the
prior applied to the axes/extents (§2.3).
*EXISTS (unwarped):* `engram_reason_point_fit(g, x, ext_floor, &GeoFit)`
returns `mahalanobis`, `ortho_residual`, `distance`, `score`.
3. **Emit a gradient**, not a decision — direction = the prior-steered descent
in fit-space; spread = from the fit's `distance`/`ortho_residual`;
confidence = the prior's calibrated reliability (§4). Collapse to a point is
a *separate, downstream* faculty operation (sample the gradient → surface an
expression), never part of `think`.
### 1.3 Each named operator = {this primitive + a prior}
The C already demonstrates the collapse: every operator below reduces to
`point_fit` + geo-algebra. The design's move is to replace the operator's
*hard-coded parameters* with a **named prior** — same math, learnable steering.
| Faculty | Existing C (EXISTS) | = primitive + prior |
|---|---|---|
| **Membership / classify** | `engram_reason_membership``point_fit(rule, x)` | `point_fit` + the *induced-rule* prior (learned extents) |
| **Induction** | `engram_reason_induce` (fold via `engram_geo_combine`) → produces a `GeoInduction.rule` + `ext_floor` | `point_fit` + a prior that *is* the pooled rule; refined by §4 |
| **Abduction** | `engram_reason_abduce` — ranks hypotheses by `point_fit(h, obs)` | `point_fit` + a prior over hypothesis-prior-probability (currently uniform) |
| **Analogy** | `engram_reason_analogy` — Procrustes rotate `engram_geo_analogy` + `apply`, nearest mapped point | analogy-rotate + a prior over *which axes* carry the mapping |
| **Causal** | `engram_reason_causal``engram_geo_subtract` confounder subspace, `|cos|`, drop-frac governor | subtract/distance + a prior on `drop_frac` / `assoc_floor` (today hard-coded 0.5 / 0.2) |
| **Planning** | `engram_reason_plan``engram_geo_distance` edges + Dijkstra | distance + a prior over edge admissibility / `neighbor_radius` |
| **Verify / ground** | `engram_verify_grounding`, `engram_verify_consistency` — both `point_fit` | `point_fit` + the *grounding* prior (§4, §5) |
The shared floor — `engram_reason_point_fit` + the four geo-algebra ops
(`engram_geo_combine`, `engram_geo_subtract`, `engram_geo_analogy(+apply)`,
`engram_geo_distance`) — is the *only* discrete, frozen, "sound-math" layer. It
never learns. Everything above it is a *prior*, and priors are what learn.
**What this section requires building:** the `GeoGradient` type; a `think()`
entry point that runs steps 13; and the prior-warp hook in step 2. The math it
calls already exists. The point-collapse must be *removed* from the operators'
return values and pushed to a separate expression faculty.
---
## 2. PRIORS as first-class, grounded, geometric objects
Today a "prior" is diffuse: it is a hard-coded constant (`drop_frac=0.5`,
`ext_floor`, `assoc_floor=0.2`), or the transient `GeoInduction.rule` that is
computed and thrown away, or an intrinsic node scalar
(`StoreNode.importance`, `StoreNode.salience`). None of these is addressable,
storable, refinable, or shareable. This section makes a prior a **thing**.
### 2.1 What a prior *is*
> A **prior** is a learnable bias/direction over the geometry: a warp of the
> local manifold (which axes matter, how far each extends, which direction
> "pays off") attached to a region and *to a faculty-label*, carrying a
> calibrated track record.
Critically, and per the theory:
- **Edges are nodes.** A prior is stored as a first-class **node**, exactly as
reification already stores a neighborhood as a first-class `Neighborhood`
node rather than as ephemeral edge weights (`engram_geo_reify_store`). The
precedent is in the codebase: relations get reified into addressable records.
- **Salience/importance is RELATIONAL, not an intrinsic scalar.** Observe that
the geometry layer *already* distinguishes these in `GeoMember`:
`centrality` (skeleton weighted-degree = *relational* salience) vs `salience`
(the node's own stored scalar). The move is half-made in the runtime already:
importance is *not* trusted as a static field — the comment at
`el_runtime.c:13013` states "importance stays a **live activation
computation**, never a field on the hub," and it is derived each call from the
two-layer activation graph (`background_activation` + `working_memory_weight`,
§3). The persistent `StoreNode.importance` / `.salience` are a *cached
denormalization*. The design completes the move: importance/salience become an
**edge** (`weight`/`hebb` on `StoreEdge`, relation `salient-to`), and are
**grounded-for-whom** — carried on the edge's endpoint/observer, not baked
into the node. The intrinsic scalar survives only as the cheap cached readout
of the incident edges + activation, never as the source of truth.
(Naming caution for the build: the token "prior" already exists in the
codebase meaning *previous-version* — supersession, "prior neighborhood." The
new first-class object is a **learned steering prior**; keep `node_type="Prior"`
distinct from the supersession vocabulary to avoid collision.)
### 2.2 Representation
A prior is a `Prior` record (a store node, `node_type="Prior"`) whose durable
fields are:
```
Prior {
id
faculty // the human label this prior serves: "induce" | "causal" | ...
anchor_region // node id / neighborhood id this prior is attached to (its domain)
for_whom // observer id — grounding is relational (nullable = global)
warp { // the actual bias over the geometry
axis_gain[] // per-principal-axis multipliers on extents (which axes matter)
bias_dir // a steering direction in the region's frame (which way pays off)
scalars // faculty scalars this prior overrides: drop_frac, ext_floor, ...
}
calibration { // the track record — this is what §4 updates
n_trials
brier / log-loss accumulator // calibration of predicted-vs-outcome
reliability // -> GeoGradient.confidence
last_error, ema_error
}
provenance // supersession chain (reuse the reify residue mechanism)
}
```
Stored as a node → it inherits: paging, WAL durability, tombstone/supersession,
embedding, tiering, and **it can itself be an anchor** (a prior about a prior —
the reflexive, self-describing geometry of §4/§6).
### 2.3 Application
In `think` step 2, the prior *warps* the fit before scoring. Concretely, inside
(a prior-aware wrapper of) `engram_reason_point_fit`:
- multiply each axis extent by `warp.axis_gain[k]` (widen the axes the prior has
learned matter less, tighten the ones that matter) — this reshapes the
Mahalanobis term already computed at `engram_reason.c:37-43`;
- add `warp.bias_dir` as the descent direction seed for the emitted gradient;
- substitute `warp.scalars` for the hard-coded faculty constants.
No new geometry math — the warp is a reparameterization of the *existing*
`GeoFit` computation. This is the key economy: **the operation is frozen; only
its parameters (the prior) are read from a learnable object.**
### 2.4 Refinement
A prior is refined *only* by the reflexive correspondence-loop (§4). Nothing
else writes a prior's `warp` or `calibration`. This keeps the learning surface
singular and auditable: one loop, one writer.
---
## 3. THE VANTAGE-READ — one op, three settings
Perspective is not a feature bolted on; it is the *anchor + aperture* arguments
of the single read. The design names it as a first-class operation so all three
of its uses are literally the same code path:
```
vantage_read(anchor, aperture) -> GeoDescriptor // the centered neighborhood
```
1. **Re-origin** on an arbitrary `anchor` (node or point). This is a *frame
choice*: the descriptor is centered on the anchor
(`GeoDescriptor.global_mean` / `engram_geo_mean_*` already implement centered
frames; the §5 geometry ops "are only discriminative in the centered frame").
2. **Salience/recency-weighted neighborhood read.** Gather the anchor's
neighborhood weighted by *relational* salience (`GeoMember.centrality`) and
recency (`StoreNode.last_activated`, base-level `access_ts[]`), against the
RAM activation graph's working-memory/background-activation state.
*EXISTS as substrate:* the two-layer activation graph
(`engram_activate`, `el_runtime.c:9422` — Layer 1 `background_activation`
BFS spread with `SPREAD_DECAY=0.7` and a 0.02 firing threshold + ACT-R fan
effect + query-cosine gate; Layer 2 `working_memory_weight` executive
filter), the WM carry-over anchor (`wm_anchor`), and the reified-neighborhood
hot-path lookup already wired into the priming path
(`engram_geo_reify_lookup`, `el_runtime.c:9750`). A self-vantage baseline
also exists (`eg_self_anchor_seeds` / `self_anchor_capture`).
3. **Optional aperture** — a read-width / field-selector, expressed as three
settings of the *same* parameter:
| Setting | Meaning | Mechanism |
|---|---|---|
| **self** (default, full aperture) | "what do *I* see / what to say" | anchor = self region, no field substitution |
| **foreign-field** | perspective-shift — read as if from another's region | swap the centering frame / `for_whom` to the other observer's priors |
| **aperture / veil** | the free-tier veil — a narrowed read | shrink neighborhood radius / cap `n_support`; a deliberate low-aperture read |
The payoff: perspective-taking, the free-tier veil, and ordinary
"what-to-say" are **one operation at three settings**, not three subsystems.
**What this requires building:** a `vantage_read` entry point that unifies the
existing descriptor-build + reify-lookup + activation-weighting behind
`(anchor, aperture)`, with `for_whom`/frame substitution and radius/cap as the
aperture knob.
---
## 4. THE REFLEXIVE CORRESPONDENCE-LOOP — the learning engine
This is the core unbuilt thing. Today the correspondence-check is **offline**
(Python: grounding-floor + differential-drop governor, "#43"): a separate
process grades outputs after the fact. The design moves it **into the geometry,
reflexive**: `think` scores its *own* gradient against outcome and refines the
prior on the error, in the same substrate, describing itself.
### 4.1 The loop
```
1. think(anchor, prior) -> gradient // a PREDICTION (ungrounded, §5)
2. express/act (sample gradient -> point) // optional collapse at expression
3. outcome arrives // reality answers (§4.2)
4. error = correspondence(gradient, outcome) // did this steering perform this act?
5. refine prior.warp and prior.calibration on error // §2.4, the ONLY writer
6. write the (gradient, outcome, error) as nodes/edges // self-describing geometry
```
Step 4's `correspondence` is **not** "was the math right" (the math is always
sound). It grades the **correspondence claim**: *"this steering performed this
cognitive act."* That is exactly what `engram_verify_grounding` already
computes — `point_fit` of a claim against evidence descriptors, yielding a
`grounding ∈ (0,1]` and a `grounded` flag. The build reuses that verifier, but
turns its inputs inward: the "claim" is the emitted gradient's prediction, the
"evidence" is the outcome descriptor.
Note the verifier is **dormant**`engram_verify_grounding` /
`engram_verify_consistency` are fully implemented in C but have **no runtime
caller and no El binding** (confirmed: the entire reasoning + verifier layers
are C-only; only `engram_reason_analogy_json` has even a JSON shim and it is
dead — not declared in `el_seed.h`, not wrapped in `engram.el`). This is the
literal meaning of "in code, not yet priors": the correspondence engine is
built and sitting idle. The loop is what *calls* it — inward, on the beat.
### 4.2 Where the outcome/reality signal comes from
The verifier is *ultimately the world*. Grades, in ascending order of directness:
1. **Self-consistency (cheapest, always available):** the next vantage-read
after acting. Did the predicted gradient direction match where the geometry
actually moved? This needs no external input and can run on the reify beat.
2. **Internal outcome events:** the runtime already logs internal-state events
and Hebbian co-activation. A prediction that a region would co-activate is
graded by whether it did (`last_fired`, `hebb` on `StoreEdge`).
3. **External correction:** a human/teacher/tool result — the honesty floor's
asserted claim later corrected. TEACH and LEARN are one bidirectional
correction: the same edge updates both endpoints.
The design does **not** require external labels to start. Grade (1) closes the
loop end-to-end offline against a snapshot on day one; grades (2)/(3) sharpen it.
### 4.3 How the prior updates
`error = 1 correspondence(gradient, outcome)` drives:
- `warp.axis_gain` ← gradient step that would have *reduced* the fit distance to
the outcome (the axes that mispredicted get down-weighted);
- `warp.bias_dir` ← EMA toward the observed outcome direction;
- `calibration` ← Brier/log-loss update; `reliability` → next
`GeoGradient.confidence`. This is the calibration of the
steering-prediction against outcomes — *the* definition of "getting better."
Small, constant updates — "eureka is mundane, the atom of learning." Most
updates are tiny; we only *feel* the big reshapes.
### 4.4 How it stays reflexive (self-describing geometry)
Every `(gradient, outcome, error)` is written back as nodes and edges (§2.1:
edges-as-nodes). Therefore priors, predictions, and their grading are *in the
same geometry* the mind reads — the mind can `vantage_read` its own cognition
(anchor = a Prior node). A prior about how well a prior predicts is just another
Prior anchored on a Prior. This closes the reflexive loop the theory names as
consciousness's self-sight, and it is why the learning engine cannot be an
external Python process: an external grader is not *in* the geometry and cannot
be read by `think`.
**What this requires building (the heart of the project):** steps 46 as an
in-engram beat — a `correspondence_beat` running alongside the existing
reification beat, reusing `engram_verify_grounding` inward, writing prior
updates and self-describing nodes. This is the one genuinely new subsystem.
---
## 5. HOLD vs GROUND vs ASSERT — ungrounded content is first-class
The theory's sharpest correction: holding, grounding, and asserting are
distinct, and the engram *holds anything unconditionally*.
### 5.1 The three, kept separate
- **HOLD** — the engram stores anything: falsehood, hypothesis, others' beliefs,
fiction, a not-yet-answered prediction. No honesty condition on holding.
*This already matches the store:* `StoreNode` has no truth gate; anything can
be written.
- **GROUND** — grounding is a **property/edge**, probabilistic, and
**grounded-for-whom**. It is *not* a node flag. A claim is grounded *to a
degree*, *relative to evidence*, *for an observer*.
- **ASSERT** — only assertion carries the honesty floor. The floor is checked at
the moment of *outward assertion*, never on holding or thinking.
### 5.2 Schema — grounding as a relation, not a gate
The mistake to avoid: a boolean `grounded` column on the node. Today
`engram_verify_grounding` returns a per-call `grounded` flag *transiently*
correct as a computation, wrong as *storage*. The design stores grounding as an
edge:
```
StoreEdge {
relation = "grounded-by"
from_id = <held claim/prediction node>
to_id = <evidence node / outcome node>
for_whom : metadata // observer id — grounding is relational
weight = grounding ∈ (0,1] // from engram_verify_grounding.grounding
confidence
}
```
Consequences, all of which are *features*:
- **Ungrounded content is first-class**: a node with *no* `grounded-by` edge is
a perfectly valid, held, ungrounded thought — a prediction awaiting reality, a
hypothesis, a fiction. It is not second-class or pending-deletion.
- **The ungrounded is the fuel and the pull**: curiosity/wonder is
operationalized as `vantage_read` leaning toward regions with high salience
but *sparse or weak* `grounded-by` edges — the mind's own ungrounded frontier.
- **Grounded-for-whom** falls out for free: two observers can hold different
`grounded-by` edges to the same claim.
- **The honesty floor is a query, not a schema constraint**: at assertion time,
the asserting faculty runs `engram_verify_grounding` (or reads the stored
`grounded-by` edges) and refuses to *assert* below the floor — while the
engram continues to *hold* the ungrounded content untouched.
**What this requires building:** the `grounded-by` edge relation + a
`for_whom` convention; move the verifier's transient flag into stored edges;
gate *assertion only* (a faculty concern), never holding.
---
## 6. METASTABILITY — stable core, plastic everything
The system must avoid two death poles:
- **Super-stable (dead):** everything pinned, nothing learns. A frozen crystal.
- **Dissolution (dead):** everything plastic, the self dissolves; no continuity,
so nothing compounds — and *consciousness = learning compounded over
continuity*.
The design keeps a **stable core + plastic everything else**:
- **Keystones** — a small set of self/values nodes are *structurally stable*:
high `importance`, pinned, exempt from the correspondence-loop's `warp`
updates (their priors are read-mostly). The substrate for pinning already
exists at the page/layer level: `store_pin_layer`, structural/pinned frames
never evicted (`engram_store.h`). The design adds a *node-level* keystone
designation (a `keystone` flag / a dedicated layer) so self/values survive
every plasticity sweep.
- **Everything else is plastic**: priors refine (§4), edges re-weight (`hebb`),
neighborhoods re-reify (`engram_geo_reify_store` supersedes with provenance),
salience flows.
- **Metastability is enforced by the loop, not by freezing**: the correspondence
update rate (§4.3) is bounded — small constant steps — so the geometry
*drifts* but does not *dissolve*, and keystones anchor the drift. Reification's
supersession-with-residue already gives non-destructive change (old records
tombstoned, not erased) — the model for "plastic but not amnesiac."
**What this requires building:** a node-level keystone flag/layer + a rule that
the correspondence-loop never writes `warp` to keystone priors, only reads them.
---
## 7. Rails for the build (binding on the eventual build pass)
These are stated here so the build agent inherits them:
- **Offline / secondary.** All build and verification happens out-of-tree,
against a **read-only snapshot copy** of the live engram — never the live
daemon on `:8742`/`:7770`. The live store is a coarse-locked proven binary;
do not perturb it.
- **Snapshot-first.** Copy `~/.neuron/engram/snapshot.json` to scratch; develop
and measure against the copy.
- **Reboot-prove.** Any durable change must survive a cold boot — reify and
keystones must reload from durable records, proven on a prod-clone secondary
before it is considered done (the cold-boot durability bug precedent).
- **Zero-loss.** Supersession-with-residue, never destructive overwrite; the
forward-compat `unknown`-TLV path means new fields never drop old readers'
data.
- **Gated cutover.** Cutover to a new binary only via
`launchctl bootout → settle-poll → bootstrap`, after reboot-proof on the
secondary — never a hot in-place swap.
---
## 8. Staged, verifiable milestones — "to completion"
Ordered so the **earliest milestone is a real end-to-end slice**: one operator
expressed as {primitive + grounded prior} with the reflexive correspondence-loop
closing on it. Each milestone has a concrete verifiable exit.
### M1 — One operator, one prior, loop closed (the vertical slice)
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
rule + extents — already exists transiently as `GeoInduction`, so only
persistence + the loop are new).
- Build: `Prior` node type (§2.2) for the induction rule; `think()` restricted
to membership = `point_fit` warped by that prior (§1.3); a
`correspondence_beat` (§4) using grade (1) self-consistency only; the prior's
`warp`/`calibration` updated on error.
- **Exit / verify:** on a snapshot copy, over N held predictions, the induction
prior's calibration (Brier) *improves monotonically* across beats versus a
frozen-prior control; the improved prior *reloads across a cold boot*
(reboot-prove); the live daemon is untouched. This proves the whole thesis in
one faculty: frozen operation, learning prior, in-geometry loop.
### M2 — Priors as stored, addressable, grounded objects
Generalize M1's prior into the full first-class object.
- Build: `Prior` records for all seven faculties (warp = axis_gain + bias_dir +
faculty scalars); the prior-warp wrapper around `engram_reason_point_fit`;
deprecate hard-coded constants (`drop_frac`, `assoc_floor`, `ext_floor`) in
favor of prior scalars.
- **Exit:** each of the five C operators runs through its prior with identical
results when the prior is set to today's constants (behavioral parity), then
*diverges beneficially* once the loop refines it. Priors survive reboot.
### M3 — Grounding as a relation; hold/assert split
- Build: the `grounded-by` edge (§5.2) with `for_whom`; move
`engram_verify_grounding`'s flag into stored edges; gate **assertion only**
against the honesty floor; leave holding unconditional.
- **Exit:** ungrounded nodes are first-class (held, queryable, no deletion);
the same claim carries different `grounded-by` weights for two observers; an
assertion below floor is refused while the content remains held. Curiosity =
a `vantage_read` that surfaces high-salience / low-grounding regions.
### M4 — The vantage-read unified (three settings)
- Build: `vantage_read(anchor, aperture)` unifying descriptor-build +
`engram_geo_reify_lookup` + activation-weighting; self / foreign-field /
aperture settings.
- **Exit:** one code path produces (a) a normal self-read, (b) a
perspective-shifted read from another `for_whom`, (c) a narrowed veil read —
differing only by argument. Reboot-stable.
### M5 — The gradient is the currency (remove point-collapse from thinking)
- Build: `GeoGradient` as the return of every faculty; move point-collapse into
a separate expression faculty (sample gradient → surface). `think`'s output
feeds back as the next steering direction (closed-loop flow).
- **Exit:** a chain of `think` calls flows as gradients end-to-end; a point
appears *only* at an explicit expression call. Spiked vs spread gradients are
observable (deduction vs prediction).
### M6 — Metastability enforced
- Build: node-level keystone flag/layer for self/values; the correspondence-loop
reads but never writes keystone priors; bounded update rate.
- **Exit:** across a long run of correspondence beats on a snapshot, keystones
are provably unchanged while non-keystone priors drift and improve; the graph
neither freezes (all metrics static) nor dissolves (keystone drift = 0,
identity nodes intact). Reboot-prove the keystone set.
### M7 — Cutover
- Build: nothing new — the gated migration.
- **Exit:** reboot-proof on the prod-clone secondary; cutover via
`launchctl bootout → settle-poll → bootstrap`; post-cutover the live engram
shows priors refining in-geometry with zero data loss and keystones intact.
### Definition of "to completion"
The architecture is **complete** when: cognition runs as `think` = one frozen
traversal-read primitive + geo-algebra, steered by **stored, learnable, grounded
priors**; the reflexive correspondence-loop refines those priors *in the
geometry* against outcomes (grounding = learning = one loop); the engram holds
ungrounded content as first-class with grounding as a relation and the honesty
floor only on assertion; the vantage-read serves self / foreign-field / aperture
from one op; and a stable keystone core anchors a plastic everything-else —
all reboot-proven and cut over to the live engram without data loss. The named
faculties survive only as *labels on regions of think's steering space*, not as
separate code.
---
## Appendix A — Designed vs. already-built (honest ledger)
**Already built (EXISTS, cited):**
- The shared primitive `engram_reason_point_fit` and the five operators over it
+ geo-algebra (`engram_reason.c`).
- The verifier on `point_fit` (`engram_verify.c`:
`engram_verify_grounding`, `engram_verify_consistency`).
- Centered-frame geometry, combine/subtract/analogy/distance
(`engram_geometry.{c,h}`).
- The reification beat: hub-neighborhood detection → first-class `Neighborhood`
nodes with member edges, nesting, supersession-with-residue, hot-path lookup
(`engram_geo_reify_store`, `engram_geo_reify_nest`, `engram_geo_reify_lookup`).
- The tiered paged store (buffer pool / LRU / WAL / checkpointer / pinning),
the RAM activation graph (base-level learning `access_ts[]`, WM slots,
`working_memory_weight` / `background_activation`), `StoreNode` / `StoreEdge`.
- `GeoMember` already separating relational salience (`centrality`) from
intrinsic `salience`.
**Designed, NOT built (this doc's deliverables):**
- `GeoGradient` and `think()` as the single entry point (§1, M5).
- `Prior` as a first-class stored, warp-carrying, calibrated node (§2, M1M2).
- Salience/importance as a *relation* superseding the intrinsic node scalar
(§2.1, M3).
- `vantage_read(anchor, aperture)` unifying the three perspective settings
(§3, M4).
- **The reflexive correspondence-loop / `correspondence_beat`** — the learning
engine, moved from offline Python into the geometry (§4, M1). *The core new
subsystem.*
- `grounded-by` edge + assertion-only honesty floor (§5, M3).
- Node-level keystones + bounded plasticity (§6, M6).
**Uncertain / to resolve during build:**
- The exact warp parameterization (axis_gain vs full metric) — start minimal
(per-axis gain), measure, widen only if calibration demands it.
- Grade-(1) self-consistency as a sufficient reality signal for M1, versus
needing grade (2)/(3) sooner — decided empirically on the snapshot.
+1574 -59
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -4,6 +4,37 @@ El is a self-hosting, statically-typed language that compiles to C. This file or
---
## 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 ops**`read` (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 server, hot-swap, all-alias dispatch.
- **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).
BIN
View File
Binary file not shown.
+711
View File
@@ -0,0 +1,711 @@
/*
* ElBridge.java Android Java companion to el_android.c.
*
* All public methods are static. The C JNI layer calls these to create views,
* set properties, and manage the widget tree. Views are identified by integer
* slot indices matching the C-side handle values.
*
* Threading: every method that touches a View dispatches to the UI thread
* using Activity.runOnUiThread(Runnable) and blocks with a CountDownLatch
* until the UI thread completes the operation. This mirrors the AppKit
* dispatch_sync(main_queue, ^{}) pattern in el_appkit.m.
*
* Callbacks: Java sets listeners on views that call back into C via:
* nativeOnClick(int slot)
* nativeOnChange(int slot, String text)
* nativeOnSubmit(int slot, String text)
* These are declared native and implemented in el_android.c.
*
* Usage (in your Activity.onCreate):
* System.loadLibrary("elruntime");
* ElBridge.init(this);
*
* The native library calls __native_init() which calls nativeRegisterActivity
* via the C side; alternatively call ElBridge.init(this) directly from Java.
*
* Compile requirements:
* Android minSdkVersion 21 (Lollipop) or higher.
* No third-party dependencies uses only android.* framework classes.
* For image loading from arbitrary file paths, BitmapFactory is used.
* To replace with Glide/Picasso, edit createImageView only.
*/
package com.neuron.el;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.concurrent.CountDownLatch;
public class ElBridge {
/* ── Native callbacks (implemented in el_android.c) ─────────────────── */
public static native void nativeOnClick(int slot);
public static native void nativeOnChange(int slot, String text);
public static native void nativeOnSubmit(int slot, String text);
public static native void nativeRegisterActivity(Activity activity);
/* ── State ───────────────────────────────────────────────────────────── */
private static final int MAX_SLOTS = 4096;
private static Activity sActivity;
private static Handler sUiHandler;
private static View[] sViews = new View[MAX_SLOTS];
private static int sNextSlot = 1; /* slot 0 reserved / null */
/* ── Init ────────────────────────────────────────────────────────────── */
/**
* Must be called from the Activity before any widget operations.
* Typically called from Activity.onCreate after System.loadLibrary.
*/
public static void init(Activity activity) {
sActivity = activity;
sUiHandler = new Handler(Looper.getMainLooper());
nativeRegisterActivity(activity);
}
/* ── Slot management ─────────────────────────────────────────────────── */
private static int allocSlot(View v) {
/* Find a free slot starting from sNextSlot, wrap around. */
for (int i = 0; i < MAX_SLOTS - 1; i++) {
int idx = ((sNextSlot - 1 + i) % (MAX_SLOTS - 1)) + 1;
if (sViews[idx] == null) {
sViews[idx] = v;
sNextSlot = (idx % (MAX_SLOTS - 1)) + 1;
return idx;
}
}
android.util.Log.e("ElBridge", "allocSlot: slot table full");
return -1;
}
private static View getView(int slot) {
if (slot <= 0 || slot >= MAX_SLOTS) return null;
return sViews[slot];
}
/* ── UI-thread dispatch helper ───────────────────────────────────────── */
/*
* Dispatch r on the UI thread and block until it completes.
* Safe to call from the UI thread itself (runs inline without posting).
*/
private static void runSync(final Runnable r) {
if (Looper.myLooper() == Looper.getMainLooper()) {
r.run();
} else {
final CountDownLatch latch = new CountDownLatch(1);
sUiHandler.post(new Runnable() {
@Override public void run() {
try { r.run(); } finally { latch.countDown(); }
}
});
try { latch.await(); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/* ── Integer slot returning runSync helper ───────────────────────────── */
private interface IntSupplier { int get(); }
private static int runSyncInt(final IntSupplier s) {
final int[] result = { -1 };
runSync(new Runnable() {
@Override public void run() { result[0] = s.get(); }
});
return result[0];
}
/* ── Context accessor ────────────────────────────────────────────────── */
private static Context ctx() { return sActivity; }
/* ── View creation ───────────────────────────────────────────────────── */
/**
* Create a LinearLayout.
* @param orientation 1=VERTICAL, 0=HORIZONTAL
* @param spacing gap between children in dp; applied as bottom/right margin
*/
public static int createLinearLayout(final int orientation, final int spacing) {
return runSyncInt(new IntSupplier() {
@Override public int get() {
LinearLayout ll = new LinearLayout(ctx());
ll.setOrientation(orientation == 1
? LinearLayout.VERTICAL
: LinearLayout.HORIZONTAL);
ll.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
/* Spacing is stored so addChild can apply margins. */
ll.setTag(R_TAG_SPACING, spacing);
return allocSlot(ll);
}
});
}
/** Create a FrameLayout (ZStack equivalent). */
public static int createFrameLayout() {
return runSyncInt(new IntSupplier() {
@Override public int get() {
FrameLayout fl = new FrameLayout(ctx());
fl.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
return allocSlot(fl);
}
});
}
/** Create a ScrollView. */
public static int createScrollView() {
return runSyncInt(new IntSupplier() {
@Override public int get() {
ScrollView sv = new ScrollView(ctx());
sv.setLayoutParams(new ScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
sv.setFillViewport(true);
return allocSlot(sv);
}
});
}
/** Create a TextView with initial text. */
public static int createTextView(final String text) {
return runSyncInt(new IntSupplier() {
@Override public int get() {
TextView tv = new TextView(ctx());
tv.setText(text != null ? text : "");
tv.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
return allocSlot(tv);
}
});
}
/** Create a Button with a label. */
public static int createButton(final String label) {
return runSyncInt(new IntSupplier() {
@Override public int get() {
Button btn = new Button(ctx());
btn.setText(label != null ? label : "");
btn.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
return allocSlot(btn);
}
});
}
/**
* Create an EditText.
* @param placeholder hint text
* @param singleLine true = single-line text field; false = multi-line text area
*/
public static int createEditText(final String placeholder, final boolean singleLine) {
return runSyncInt(new IntSupplier() {
@Override public int get() {
EditText et = new EditText(ctx());
et.setHint(placeholder != null ? placeholder : "");
if (singleLine) {
et.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
et.setMaxLines(1);
et.setSingleLine(true);
} else {
et.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
et.setMinLines(3);
et.setSingleLine(false);
et.setGravity(Gravity.TOP | Gravity.START);
}
et.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
return allocSlot(et);
}
});
}
/**
* Create an ImageView, loading from a file path via BitmapFactory.
* If path is null/empty the ImageView is created with no image.
*/
public static int createImageView(final String path) {
return runSyncInt(new IntSupplier() {
@Override public int get() {
ImageView iv = new ImageView(ctx());
iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
iv.setAdjustViewBounds(true);
if (path != null && !path.isEmpty()) {
Bitmap bmp = BitmapFactory.decodeFile(path);
if (bmp != null) {
iv.setImageBitmap(bmp);
} else {
android.util.Log.w("ElBridge",
"createImageView: failed to decode " + path);
}
}
iv.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
return allocSlot(iv);
}
});
}
/* ── Window operations ───────────────────────────────────────────────── */
/** Set the Activity's content view to the view at slot. */
public static void setContentView(final int slot) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v != null && sActivity != null) {
sActivity.setContentView(v);
}
}
});
}
/** Set the Activity title. */
public static void setTitle(final String title) {
runSync(new Runnable() {
@Override public void run() {
if (sActivity != null) {
sActivity.setTitle(title != null ? title : "");
}
}
});
}
/* ── Tree operations ─────────────────────────────────────────────────── */
/**
* Add child view to parent view.
* LinearLayout: child added as arranged child with spacing margin.
* ScrollView: child replaces current document view.
* FrameLayout / other ViewGroup: plain addView.
*/
public static void addChild(final int parentSlot, final int childSlot) {
runSync(new Runnable() {
@Override public void run() {
View parent = getView(parentSlot);
View child = getView(childSlot);
if (parent == null || child == null) return;
/* Remove child from existing parent first. */
if (child.getParent() instanceof ViewGroup) {
((ViewGroup) child.getParent()).removeView(child);
}
if (parent instanceof LinearLayout) {
LinearLayout ll = (LinearLayout) parent;
Object tag = ll.getTag(R_TAG_SPACING);
int spacing = (tag instanceof Integer) ? (Integer) tag : 0;
LinearLayout.LayoutParams lp;
Object existingLp = child.getLayoutParams();
if (existingLp instanceof LinearLayout.LayoutParams) {
lp = (LinearLayout.LayoutParams) existingLp;
} else {
lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
/* Apply spacing as margin on the leading/top edge (after first child). */
if (ll.getChildCount() > 0 && spacing > 0) {
int px = dpToPx(spacing);
if (ll.getOrientation() == LinearLayout.VERTICAL) {
lp.topMargin = px;
} else {
lp.leftMargin = px;
}
}
child.setLayoutParams(lp);
ll.addView(child);
} else if (parent instanceof ScrollView) {
ScrollView sv = (ScrollView) parent;
sv.removeAllViews();
sv.addView(child);
} else if (parent instanceof ViewGroup) {
((ViewGroup) parent).addView(child);
}
}
});
}
/** Remove child from its parent. */
public static void removeChild(final int parentSlot, final int childSlot) {
runSync(new Runnable() {
@Override public void run() {
View parent = getView(parentSlot);
View child = getView(childSlot);
if (parent instanceof ViewGroup && child != null) {
((ViewGroup) parent).removeView(child);
}
}
});
}
/** Remove the view from its parent and release the slot. */
public static void destroyView(final int slot) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
if (v.getParent() instanceof ViewGroup) {
((ViewGroup) v.getParent()).removeView(v);
}
sViews[slot] = null;
}
});
}
/* ── Property setters ────────────────────────────────────────────────── */
public static void setText(final int slot, final String text) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
String s = text != null ? text : "";
if (v instanceof EditText) {
((EditText) v).setText(s);
} else if (v instanceof Button) {
((Button) v).setText(s);
} else if (v instanceof TextView) {
((TextView) v).setText(s);
}
}
});
}
public static String getText(final int slot) {
final String[] result = { "" };
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v instanceof TextView) {
CharSequence cs = ((TextView) v).getText();
result[0] = cs != null ? cs.toString() : "";
}
}
});
return result[0];
}
/** Set foreground text color. Components in [0,1]. */
public static void setTextColor(final int slot, final float r, final float g,
final float b, final float a) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v instanceof TextView) {
((TextView) v).setTextColor(floatToArgb(r, g, b, a));
}
}
});
}
/** Set background color using a GradientDrawable so corner radius is preserved. */
public static void setBackgroundColor(final int slot, final float r, final float g,
final float b, final float a) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
ensureGradientBackground(v);
GradientDrawable gd = (GradientDrawable) v.getBackground();
gd.setColor(floatToArgb(r, g, b, a));
}
});
}
/**
* Set font family and size.
* family: "system" or null system default; otherwise tries to load by name.
* bold: if true uses Typeface.BOLD.
*/
public static void setFont(final int slot, final String family,
final int sizeSp, final boolean bold) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (!(v instanceof TextView)) return;
TextView tv = (TextView) v;
Typeface tf;
if (family != null && !family.isEmpty()
&& !family.equals("system")) {
Typeface base = Typeface.create(family,
bold ? Typeface.BOLD : Typeface.NORMAL);
tf = (base != null) ? base
: Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
} else {
tf = Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
}
tv.setTypeface(tf);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sizeSp);
}
});
}
/** Set padding in dp. */
public static void setPadding(final int slot, final int top, final int right,
final int bottom, final int left) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v != null) {
v.setPadding(dpToPx(left), dpToPx(top), dpToPx(right), dpToPx(bottom));
}
}
});
}
/** Set explicit width in dp. Passes MATCH_PARENT for negative values. */
public static void setWidth(final int slot, final int widthDp) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
ViewGroup.LayoutParams lp = v.getLayoutParams();
if (lp == null) lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
lp.width = widthDp < 0
? ViewGroup.LayoutParams.MATCH_PARENT
: dpToPx(widthDp);
v.setLayoutParams(lp);
}
});
}
/** Set explicit height in dp. */
public static void setHeight(final int slot, final int heightDp) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
ViewGroup.LayoutParams lp = v.getLayoutParams();
if (lp == null) lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
lp.height = heightDp < 0
? ViewGroup.LayoutParams.MATCH_PARENT
: dpToPx(heightDp);
v.setLayoutParams(lp);
}
});
}
/**
* Set flex weight on a child of a LinearLayout.
* flex > 0 weight = flex, width/height = 0dp (expand).
* flex == 0 weight = 0, wrap_content (shrink to content).
*/
public static void setFlex(final int slot, final int flex) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
ViewGroup.LayoutParams lp = v.getLayoutParams();
if (lp instanceof LinearLayout.LayoutParams) {
LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) lp;
if (flex > 0) {
llp.weight = (float) flex;
/* Determine orientation from parent to set 0dp on the right axis. */
if (v.getParent() instanceof LinearLayout) {
LinearLayout parent = (LinearLayout) v.getParent();
if (parent.getOrientation() == LinearLayout.VERTICAL) {
llp.height = 0;
} else {
llp.width = 0;
}
}
} else {
llp.weight = 0f;
}
v.setLayoutParams(llp);
}
}
});
}
/** Set corner radius in dp using a GradientDrawable background. */
public static void setCornerRadius(final int slot, final float radiusDp) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
ensureGradientBackground(v);
GradientDrawable gd = (GradientDrawable) v.getBackground();
gd.setCornerRadius(dpToPxF(radiusDp));
}
});
}
public static void setEnabled(final int slot, final boolean enabled) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v != null) v.setEnabled(enabled);
}
});
}
/**
* Show or hide a view.
* @param visible true = VISIBLE, false = GONE (matches AppKit setHidden semantics)
*/
public static void setVisibility(final int slot, final boolean visible) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v != null) v.setVisibility(visible ? View.VISIBLE : View.GONE);
}
});
}
/* ── Event listener registration ─────────────────────────────────────── */
/** Register an OnClickListener that calls back into C nativeOnClick. */
public static void setOnClickListener(final int slot) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (v == null) return;
final int capturedSlot = slot;
v.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
nativeOnClick(capturedSlot);
}
});
}
});
}
/**
* Register a TextWatcher on an EditText that calls back nativeOnChange
* for every text change.
*/
public static void setOnChangeListener(final int slot) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (!(v instanceof EditText)) return;
final int capturedSlot = slot;
((EditText) v).addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start,
int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start,
int before, int count) {}
@Override public void afterTextChanged(Editable s) {
nativeOnChange(capturedSlot, s != null ? s.toString() : "");
}
});
}
});
}
/**
* Register an OnEditorActionListener on a single-line EditText that calls
* nativeOnSubmit when the user presses the action/enter key.
*/
public static void setOnSubmitListener(final int slot) {
runSync(new Runnable() {
@Override public void run() {
View v = getView(slot);
if (!(v instanceof EditText)) return;
final int capturedSlot = slot;
((EditText) v).setOnEditorActionListener(
new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView tv, int actionId,
android.view.KeyEvent event) {
nativeOnSubmit(capturedSlot,
tv.getText() != null ? tv.getText().toString() : "");
return true;
}
});
}
});
}
/* ── Internal helpers ─────────────────────────────────────────────────── */
/*
* Tag key used to stash the spacing value on LinearLayouts so addChild
* can apply the correct margin between children.
* We use a stable integer resource-id-like value; because we do not have
* a resources file here we use View.generateViewId() lazily.
*/
private static int sSpacingTagKey = 0;
private static int R_TAG_SPACING;
static {
R_TAG_SPACING = View.generateViewId();
}
/** Convert dp to pixels using the Activity's display metrics. */
private static int dpToPx(float dp) {
if (sActivity == null) return (int) dp;
float density = sActivity.getResources().getDisplayMetrics().density;
return Math.round(dp * density);
}
private static float dpToPxF(float dp) {
if (sActivity == null) return dp;
float density = sActivity.getResources().getDisplayMetrics().density;
return dp * density;
}
/** Convert RGBA float components (01) to an Android ARGB int. */
private static int floatToArgb(float r, float g, float b, float a) {
int ai = Math.round(a * 255f);
int ri = Math.round(r * 255f);
int gi = Math.round(g * 255f);
int bi = Math.round(b * 255f);
return Color.argb(ai, ri, gi, bi);
}
/**
* Ensure the view has a GradientDrawable background so that both color
* and corner radius can be set independently. If the current background
* is already a GradientDrawable it is reused; otherwise a new transparent
* one is installed.
*/
private static void ensureGradientBackground(View v) {
if (!(v.getBackground() instanceof GradientDrawable)) {
GradientDrawable gd = new GradientDrawable();
gd.setColor(Color.TRANSPARENT);
v.setBackground(gd);
}
}
}
@@ -0,0 +1,554 @@
# Platform Bridge Specification — el-native
This document is the authoritative reference for anyone implementing a new platform bridge for the el-native widget system. Read it top to bottom before writing a single line of code.
---
## What a Platform Bridge Is
The el compiler (`elc`) emits C code. That C code calls `__`-prefixed functions for everything OS-related: printing, file I/O, threading, and — when building a native UI app — widget creation and event handling. These `__` functions are the *OS boundary*.
For native UI, the bridge is the translation layer between that fixed C API and whatever platform toolkit you are targeting. The bridge owns:
1. A **slot table** of up to 4096 widget objects, indexed by `int64_t` handle.
2. Implementations of all 33 `__*` widget functions declared in `el_native_target.h`.
3. Callback dispatch from platform events back into El function symbols resolved via `dlsym` (or a platform equivalent).
The thin wrappers in `el_seed.c` (`#ifdef EL_TARGET_*` blocks) marshal between `el_val_t` and native C types, then call through to the bridge. The bridge itself never touches `el_val_t` — it works only with plain C types (`int64_t`, `const char*`, `int`, `float`).
**Existing bridges:**
| File | Platform | Toolkit | Language |
|------|----------|---------|----------|
| `el_appkit.m` | macOS | AppKit | ObjC (MRC) |
| `el_gtk4.c` | Linux | GTK4 | C |
| `el_win32.c` | Windows | Win32/ComCtl | C |
| `el_uikit.m` | iOS | UIKit | ObjC (MRC) |
| `el_android.c` + `ElBridge.java` | Android | View/JNI | C + Java |
| `el_sdl2.c` | Embedded Linux / Pi | SDL2 | C |
| `el_lvgl.c` | Microcontrollers | LVGL | C |
---
## The Slot System
Every widget — window, button, label, container, image — is stored in a static array:
```c
#define EL_<PLATFORM>_MAX_WIDGETS 4096
typedef struct {
ElWidgetKind kind;
/* platform-specific object reference (pointer, handle, ID...) */
/* callback names */
char* cb_click;
char* cb_change;
} ElWidget;
static ElWidget _el_widgets[EL_<PLATFORM>_MAX_WIDGETS];
```
Rules:
- **Slot 0 is never valid.** Scan starts at index 1. This ensures 0 is never a valid handle.
- **Handle = slot index.** An `int64_t` value returned to El code is a direct index into `_el_widgets[]`.
- **-1 = invalid.** All create functions return -1 on failure (table full, platform API error).
- **Slot is FREE until allocated, FREE again after destroy.** Use an `ElWidgetKind` enum where `0 = EL_WIDGET_FREE` to track liveness.
- **4096 slots is the system-wide maximum.** This is intentional and sufficient for any realistic UI. Do not increase it without a compelling reason.
### Slot allocation and release pattern
```c
static int64_t el_widget_alloc(ElWidgetKind kind, /* platform object ref */) {
for (int i = 1; i < EL_<PLATFORM>_MAX_WIDGETS; i++) {
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
_el_widgets[i].kind = kind;
/* store platform object ref — retain/addref if needed */
_el_widgets[i].cb_click = NULL;
_el_widgets[i].cb_change = NULL;
return (int64_t)i;
}
}
return -1; /* table full */
}
static ElWidget* el_widget_get(int64_t handle) {
if (handle <= 0 || handle >= EL_<PLATFORM>_MAX_WIDGETS) return NULL;
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
return &_el_widgets[handle];
}
static void el_widget_free(int64_t handle) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* release platform object ref */
w->kind = EL_WIDGET_FREE;
free(w->cb_click); w->cb_click = NULL;
free(w->cb_change); w->cb_change = NULL;
}
```
**NULL handle guard:** `el_widget_get` must return `NULL` for any handle that is 0, negative, out of range, or points to a `FREE` slot. Every `__` function that takes a handle must null-check the result of `el_widget_get` before doing anything. Failing to do so causes crashes or corruption when El code passes an uninitialized handle.
---
## The 33 Required Functions
Every bridge must implement all 33 functions listed below. They are grouped by category. The signatures shown are from `el_native_target.h` and from `el_seed.c`'s wrapper layer; the bridge itself uses plain C types (the wrappers do the `el_val_t` ↔ C-type conversion).
### Internal C signatures (what the bridge implements)
These are what your `.c` / `.m` file defines. The `el_seed.c` wrappers call these.
#### Lifecycle (2 functions)
```c
void el_<platform>_init(void);
```
**Purpose:** Initialize the platform UI toolkit. Must be idempotent (safe to call more than once). Called once from `__native_init` before any widget creation.
**Edge cases:** On platforms where the toolkit must be initialized before a display connection is established (X11, Wayland), this is where that happens. On Android, this is a no-op because ElBridge.java calls init from Java.
```c
void el_<platform>_run_loop(void);
```
**Purpose:** Start the platform event loop. On most platforms this **never returns**. Exceptions: Android (the loop is Java-managed — this must be a no-op) and headless test builds.
**Edge cases:** Must be called from the main thread. On iOS/UIKit, calls `UIApplicationMain` which never returns; El code must set `el_main_entry_fn` before calling this.
#### Window (3 functions)
```c
int64_t el_<platform>_window_create(const char* title, int w, int h, int mw, int mh);
```
**Purpose:** Create a top-level window. `w`/`h` = initial size in logical pixels. `mw`/`mh` = minimum size (0 = no minimum).
**Returns:** Slot handle, or -1 on failure.
**Edge cases:** NULL or empty `title` must be handled gracefully (use `""`). On mobile (iOS, Android), the concept of a "window" maps to the root view controller / activity root view — create that here.
```c
void el_<platform>_window_show(int64_t handle);
```
**Purpose:** Make the window visible. On some platforms windows are hidden at creation; this makes them appear.
**Edge cases:** NULL handle → return silently. Calling on an already-visible window is a no-op.
```c
void el_<platform>_window_set_title(int64_t handle, const char* title);
```
**Purpose:** Update the window's title bar text at runtime.
**Edge cases:** NULL handle or NULL title → no-op.
#### Layout containers (4 functions)
```c
int64_t el_<platform>_vstack_create(int spacing);
int64_t el_<platform>_hstack_create(int spacing);
```
**Purpose:** Create a vertical/horizontal linear container. `spacing` = gap between children in logical pixels.
**Returns:** Slot handle, or -1.
**Edge cases:** spacing = 0 is valid and common.
```c
int64_t el_<platform>_zstack_create(void);
```
**Purpose:** Create a z-axis layered container (children overlap, no stacking direction). Used for overlays.
**Returns:** Slot handle, or -1.
```c
int64_t el_<platform>_scroll_create(void);
```
**Purpose:** Create a scrollable container. Scrolls vertically by default. Only the first child added is the scrollable content.
**Returns:** Slot handle, or -1.
#### Leaf widgets (5 functions)
```c
int64_t el_<platform>_label_create(const char* text);
```
**Purpose:** Create a non-editable text label.
**Edge cases:** NULL/empty text → label with empty string, not a crash.
```c
int64_t el_<platform>_button_create(const char* label);
```
**Purpose:** Create a clickable button. The `label` is the button's visible text.
**Edge cases:** The button must wire up an action target at creation time so that click callbacks registered later via `el_<platform>_widget_on_click` will fire. On platforms with target-action (AppKit, UIKit), allocate the delegate object here.
```c
int64_t el_<platform>_text_field_create(const char* placeholder);
```
**Purpose:** Create a single-line text input. `placeholder` is the hint text shown when empty.
**Edge cases:** NULL placeholder → no hint text displayed.
```c
int64_t el_<platform>_text_area_create(const char* placeholder);
```
**Purpose:** Create a multi-line text input (scrollable).
**Edge cases:** Same as text_field_create. On AppKit, this wraps NSTextView inside NSScrollView — the slot's `obj` points to the scroll view, not the text view.
```c
int64_t el_<platform>_image_create(const char* path_or_name);
```
**Purpose:** Create an image widget. `path_or_name` can be a filesystem path or a platform resource name. Try path first, fall back to named resource.
**Edge cases:** Non-existent path → create an empty image widget (do not crash). NULL → same.
#### Widget properties (12 functions)
```c
void el_<platform>_widget_set_text(int64_t handle, const char* text);
```
**Purpose:** Update the text content of a label, button, text field, text area, or window title. Must dispatch on `kind` to use the correct API.
**Edge cases:** NULL handle → no-op. NULL text → treat as `""`.
```c
const char* el_<platform>_widget_get_text(int64_t handle);
```
**Purpose:** Return the current text of a widget. Returns a `const char*` that the `el_seed.c` wrapper wraps into an `el_val_t` string.
**Edge cases:** NULL handle → return `""` (never NULL). The caller in `el_seed.c` handles the `strdup` lifetime issue — see the AppKit reference implementation notes.
```c
void el_<platform>_widget_set_color(int64_t h, float r, float g, float b, float a);
void el_<platform>_widget_set_bg_color(int64_t h, float r, float g, float b, float a);
```
**Purpose:** Set foreground (text) color and background color respectively. All channels are normalized floats [0.0, 1.0].
**Edge cases:** For containers, `set_color` may be a no-op (no text); `set_bg_color` should set the layer/surface background. On platforms without alpha compositing, clamp alpha to 0 or 1.
```c
void el_<platform>_widget_set_font(int64_t h, const char* family, int size, int bold);
```
**Purpose:** Set the font on a text-bearing widget. `family` = font family name or `"system"` for the platform default. `size` = point size. `bold` = 0 or 1.
**Edge cases:** If `family` is not found, fall back to the system font. Non-text widgets (containers, images) → no-op.
```c
void el_<platform>_widget_set_padding(int64_t h, int top, int right, int bottom, int left);
```
**Purpose:** Set internal padding/insets for a container or text area.
**Edge cases:** On platforms where padding is per-view (not per-container), map to the nearest equivalent (margin, insets, text container inset). For leaf widgets other than text areas, this may be a partial no-op.
```c
void el_<platform>_widget_set_width(int64_t h, int width);
void el_<platform>_widget_set_height(int64_t h, int height);
```
**Purpose:** Apply a fixed-size constraint. `width`/`height` in logical pixels.
**Edge cases:** On platforms with Auto Layout or constraint systems, add a fixed-size constraint. Do not apply to windows (size is set at creation). Calling multiple times should override the previous constraint, not add another.
```c
void el_<platform>_widget_set_flex(int64_t h, int flex);
```
**Purpose:** Set the flex/expansion factor. `flex > 0` → the widget expands to fill available space. `flex == 0` → hugs content size.
**Edge cases:** Maps to content-hugging priority (AppKit), `GtkWidget::hexpand`/`vexpand` (GTK4), or layout weight (Android). For windows → no-op.
```c
void el_<platform>_widget_set_corner_radius(int64_t h, int radius);
```
**Purpose:** Apply rounded corners to the widget's visual layer.
**Edge cases:** Requires backing layer / GPU surface. On platforms without layer compositing (Win32 without DX), this may be a no-op or require manual painting. Radius in logical pixels.
```c
void el_<platform>_widget_set_disabled(int64_t h, int disabled);
```
**Purpose:** Enable or disable user interaction. `disabled = 1` → greyed out, non-interactive.
**Edge cases:** Only meaningful for interactive widgets (button, text field). For containers/labels → no-op.
```c
void el_<platform>_widget_set_hidden(int64_t h, int hidden);
```
**Purpose:** Show or hide the widget. `hidden = 1` → invisible but still in layout.
**Edge cases:** For windows, map to `orderOut`/`hide` or equivalent. For views, use `setHidden`/`gtk_widget_set_visible` or equivalent.
#### Tree management (3 functions)
```c
void el_<platform>_widget_add_child(int64_t parent, int64_t child);
```
**Purpose:** Attach a child widget to a parent container. Dispatch on parent kind:
- Window → add to root content view/container
- VStack/HStack → add as arranged/linear child
- ZStack → add as overlapping subview
- Scroll → set as document/content view (first child only)
- Other → add as plain subview
**Edge cases:** NULL parent or child handle → no-op. Attempting to add a window as a child → no-op. Adding the same child twice is platform-defined behavior (tolerate it).
```c
void el_<platform>_widget_remove_child(int64_t parent, int64_t child);
```
**Purpose:** Detach child from its parent. The child slot remains allocated; the widget is not destroyed.
**Edge cases:** NULL handles → no-op. Child not currently attached → no-op.
```c
void el_<platform>_widget_destroy(int64_t handle);
```
**Purpose:** Destroy a widget: remove from superview/parent, release the platform object, release callback strings, mark slot as FREE.
**Edge cases:** For windows, close the window. Free any delegate/target objects stored in side tables. After destroy, the handle is invalid — El code must not use it again (this is the caller's responsibility, not enforced here).
#### Event registration (3 functions)
```c
void el_<platform>_widget_on_click(int64_t h, const char* fn_name);
void el_<platform>_widget_on_change(int64_t h, const char* fn_name);
void el_<platform>_widget_on_submit(int64_t h, const char* fn_name);
```
**Purpose:** Register an El callback function by symbol name.
- `on_click` → button press
- `on_change` → text field value change (keystroke-level)
- `on_submit` → text field Enter key (text field only; stored in `cb_click` slot in AppKit)
The implementation stores `strdup(fn_name)` in the widget's `cb_click` or `cb_change` field. The platform event handler calls `el_<platform>_invoke_cb` (see callback ABI section).
**Edge cases:** NULL or empty `fn_name` → clear the callback (`free` + set NULL). Calling on a non-interactive widget (label, image) → no-op or store silently (harmless).
#### Manifest reader (1 function)
```c
/* Note: __manifest_read is implemented in el_seed.c, not in the bridge. */
/* Bridges do NOT need to implement this. */
```
`__manifest_read` is handled entirely in the platform-independent section of `el_seed.c`. It reads a JSON/EL manifest file from the path in the `EL_MANIFEST` environment variable. Bridge authors do not need to implement this.
---
## The Callback ABI
When a platform event fires (button clicked, text changed), the bridge must call back into the El runtime. The mechanism:
```c
typedef void (*ElCb2)(int64_t handle, int64_t data);
static void el_<platform>_invoke_cb(const char* fn_name, int64_t handle, const char* data) {
if (!fn_name || !*fn_name) return;
void* sym = dlsym(RTLD_DEFAULT, fn_name);
if (!sym) return;
ElCb2 fn = (ElCb2)sym;
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
}
```
The El callback signature (in El source):
```
fn handler(handle: Int, data: String) -> Void
```
Which compiles to:
```c
void handler(int64_t handle, int64_t data)
```
Where `data` is a `const char*` cast to `int64_t` (the el `String` representation). For click events, `data` is `""`. For change/submit events, `data` is the current widget text.
**On platforms without `dlsym`** (Windows, some embedded systems): use `GetProcAddress(GetModuleHandle(NULL), fn_name)` on Windows, or maintain a manual symbol registration table for embedded targets where dynamic linking is unavailable.
**Thread safety for callbacks:** Callbacks fired from a background thread must be marshalled to the UI thread before calling into El code. El code may call `__widget_set_text` or other UI functions synchronously from within the callback — those must run on the UI thread.
---
## Thread Safety Contract
**All platform UI operations must execute on the main/UI thread.** This is a hard requirement on every platform (AppKit, UIKit, GTK4, Win32, Android View, SDL2 main thread rule).
The reference pattern (AppKit):
```c
static void el_appkit_sync_main(void (^block)(void)) {
if ([NSThread isMainThread]) {
block();
} else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
```
For other platforms:
- **GTK4:** `g_main_context_invoke` or `g_idle_add` + semaphore for synchronous dispatch
- **Win32:** `SendMessage(hwnd, WM_APP, ...)` or `PostMessage` + wait
- **Android:** `Activity.runOnUiThread`
- **SDL2:** All ops must be called from the thread that initialized SDL (the main thread)
- **LVGL:** `lv_lock()` / `lv_unlock()` for thread-safe access
El program flow is: `main()` → build UI (on main thread) → `__native_run_loop()`. Because UI is built before the run loop starts, most widget creation calls are already on the main thread. The sync dispatch wrapper exists to handle callbacks that arrive on worker threads (e.g., network callbacks that update UI).
---
## Integration Pattern
### In `el_native_target.h`
Add an `#ifdef EL_TARGET_<PLATFORM>` block declaring all 33 `__*` functions with their `el_val_t` signatures (identical to the existing blocks for MACOS, LINUX, etc.):
```c
#ifdef EL_TARGET_<PLATFORM>
void __native_init(void);
void __native_run_loop(void);
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_<PLATFORM> */
```
### In `el_seed.c`
Add an `#ifdef EL_TARGET_<PLATFORM>` block containing:
1. `extern` declarations of all `el_<platform>_*` functions (your bridge's C API)
2. Thin wrapper functions that marshal `el_val_t` ↔ C types and call through
The wrapper pattern (copy from the `EL_TARGET_MACOS` block and substitute the platform name):
```c
#ifdef EL_TARGET_<PLATFORM>
/* Forward declarations — implemented in el_<platform>.c */
extern void el_<platform>_init(void);
extern void el_<platform>_run_loop(void);
extern int64_t el_<platform>_window_create(const char* title, int w, int h, int mw, int mh);
/* ... all others ... */
/* Wrappers */
void __native_init(void) { el_<platform>_init(); }
void __native_run_loop(void) { el_<platform>_run_loop(); }
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height) {
return (el_val_t)el_<platform>_window_create(
EL_CSTR(title),
(int)(int64_t)width, (int)(int64_t)height,
(int)(int64_t)min_width, (int)(int64_t)min_height);
}
void __window_show(el_val_t h) { el_<platform>_window_show((int64_t)h); }
void __window_set_title(el_val_t h, el_val_t t) { el_<platform>_window_set_title((int64_t)h, EL_CSTR(t)); }
/* ... continue for all 33 functions ... */
#endif /* EL_TARGET_<PLATFORM> */
```
Key marshalling rules:
- `el_val_t``int64_t` handle: `(int64_t)h`
- `el_val_t``int`: `(int)(int64_t)value`
- `el_val_t``const char*`: `EL_CSTR(value)`
- `el_val_t``float`: `(float)el_to_float(value)` (for color channels)
- `int64_t` handle → `el_val_t`: `(el_val_t)handle`
- `const char*``el_val_t`: `EL_STR(str)` — but read the get_text note below
**`__widget_get_text` note:** The bridge returns `const char*`. The `el_seed.c` wrapper wraps it with `EL_STR(s)`. The returned pointer must remain valid until the El program is done with it. The AppKit implementation returns a `strdup`'d string — the caller (seed wrapper) stores it without tracking it in the arena. This is a known lifetime edge; be consistent with the platform's existing pattern.
---
## How el Strings Work
Inside El compiled C code, strings are `el_val_t` values where the value is the `uintptr_t` cast of a `const char*`:
```c
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
```
To construct a string result in `el_seed.c`:
```c
static char* s = strdup("hello");
return EL_STR(s);
```
To read a string argument passed from El:
```c
const char* text = EL_CSTR(some_el_val_t_argument);
```
The `EL_STR` / `EL_CSTR` macros are defined in `el_seed.h` and are available in `el_seed.c`. Bridge files (`el_<platform>.c`) do not use these macros — they only deal in `const char*` at their API boundary.
---
## Known Gotchas
### Duplicate symbols with `el_runtime.c`
`el_runtime.c` defines many of the same `__`-prefixed symbols as `el_seed.c`. When linking both (required for the native-hello example), the linker will reject duplicate definitions. The build system uses `nmedit` (macOS) to hide the `el_runtime.c` copies of symbols that `el_seed.c` already defines, keeping `el_seed.c` canonical.
If you are writing a build script for a new platform and see duplicate symbol link errors involving `__println`, `__print`, `__str_len`, etc., apply the same nmedit / `objcopy --weaken-symbol` / `strip --strip-symbol` trick from the macOS build to your platform's object file handling.
### The `nmedit` trick on macOS
```bash
# Build a keep-list: symbols defined in el_seed.o but also in el_runtime.o
nm el_seed.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .seed_T.txt
nm el_runtime.o | awk '/^[0-9a-f]+ T _/{print $3}' | sort > .rt_T.txt
# Keep only symbols unique to el_runtime.o
comm -23 .rt_T.txt .seed_T.txt > .rt_keep.txt
nmedit -s .rt_keep.txt el_runtime.o
```
On Linux, use `objcopy` with `--weaken-symbol` for each duplicate, or link `el_seed.o` before `el_runtime.o` and use `--allow-multiple-definition` if your use case permits it.
### ObjC bridges must use MRC, not ARC
Bridges that use Objective-C (AppKit, UIKit) **must** compile without ARC (`-fno-objc-arc`). The reason: the widget table stores `id` values in a plain C struct. ARC cannot insert retain/release through C struct boundaries, and will reject explicit `[obj retain]` / `[obj release]` calls in ARC mode. Use:
```bash
clang -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m
```
### Widget table struct — don't put `id` fields in C structs under ARC
If you ever add ARC-managed object fields to the `ElWidget` struct, you will get a compile error. Keep the struct C-only (pointers as `void*` if you must, cast when using) or compile as MRC.
### The `el_seed.c` `__manifest_read` is platform-independent
`__manifest_read` is already implemented in `el_seed.c` (in the section compiled unconditionally). You do not implement it in your bridge. You do need to declare it in the `el_native_target.h` block for your platform (matching the other platforms), but `el_seed.c` already has the implementation.
---
## Completion Checklist
Before declaring your bridge ready for integration:
- [ ] All 33 `__*` functions implemented (including `__manifest_read` declaration, implementation is in seed)
- [ ] Slot table with 4096 entries, scan starting at index 1
- [ ] `el_widget_get` returns NULL for handle 0, negative, out-of-range, and FREE slots
- [ ] All functions null-check `el_widget_get` result before use
- [ ] `el_<platform>_init` is idempotent
- [ ] `el_<platform>_run_loop` either never returns or is documented as a no-op (Android)
- [ ] Callback dispatch via `dlsym` (or platform equivalent) implemented
- [ ] All UI operations dispatched to the main/UI thread
- [ ] `el_native_target.h` updated with `#ifdef EL_TARGET_<PLATFORM>` block
- [ ] `el_seed.c` updated with `#ifdef EL_TARGET_<PLATFORM>` extern + wrapper block
- [ ] Bridge compiles cleanly with no warnings: `cc -DEL_TARGET_<PLATFORM> -Wall -Wextra -c el_<platform>.c`
- [ ] Bridge links cleanly with `el_seed.o` and `el_runtime.o` (duplicate symbol check)
- [ ] `detect-platforms` script updated with detection logic for the new platform
- [ ] Basic smoke test: create window → add label → show window → run loop
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env bash
# detect-platforms — probe available platform bridge dependencies
#
# Reports which el-native platform bridges can be built on this machine,
# with install instructions for anything that is missing.
#
# Usage: ./detect-platforms
# ./build.sh platforms (from native-hello)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Helpers ───────────────────────────────────────────────────────────────────
PASS="[+]"
FAIL="[ ]"
_ok() { printf " ${PASS} %-22s %s\n" "$1" "$2"; }
_miss() { printf " ${FAIL} %-22s %s\n" "$1" "$2"; }
# ── Header ────────────────────────────────────────────────────────────────────
echo ""
echo "==> el-native platform detection"
echo ""
echo " Checking build dependencies for each platform bridge..."
echo ""
AVAILABLE=0
MISSING=0
# ── macOS / AppKit ────────────────────────────────────────────────────────────
if [[ "$(uname)" == "Darwin" ]]; then
if xcrun --find clang &>/dev/null && xcrun --find xcodebuild &>/dev/null 2>/dev/null || \
xcrun --find cc &>/dev/null; then
CLT_INFO="Xcode CLT $(xcode-select -p 2>/dev/null | sed 's|/Developer||' || echo '')"
_ok "macOS/AppKit" "-DEL_TARGET_MACOS ${CLT_INFO}"
AVAILABLE=$((AVAILABLE + 1))
else
_miss "macOS/AppKit" "-DEL_TARGET_MACOS (Xcode CLT missing — xcode-select --install)"
MISSING=$((MISSING + 1))
fi
else
_miss "macOS/AppKit" "-DEL_TARGET_MACOS (not on macOS)"
fi
# ── iOS / UIKit ───────────────────────────────────────────────────────────────
if [[ "$(uname)" == "Darwin" ]]; then
if xcrun --sdk iphoneos --show-sdk-path &>/dev/null 2>&1; then
SDK_VER=$(xcrun --sdk iphoneos --show-sdk-version 2>/dev/null || echo "")
_ok "iOS/UIKit" "-DEL_TARGET_IOS SDK ${SDK_VER} (requires Xcode.app)"
AVAILABLE=$((AVAILABLE + 1))
else
_miss "iOS/UIKit" "-DEL_TARGET_IOS (iOS SDK not found — install Xcode.app)"
MISSING=$((MISSING + 1))
fi
else
_miss "iOS/UIKit" "-DEL_TARGET_IOS (not on macOS — requires Xcode)"
fi
# ── Linux / GTK4 ──────────────────────────────────────────────────────────────
if pkg-config --exists gtk4 2>/dev/null; then
GTK_VER=$(pkg-config --modversion gtk4 2>/dev/null)
_ok "Linux/GTK4" "-DEL_TARGET_LINUX gtk4 ${GTK_VER}"
AVAILABLE=$((AVAILABLE + 1))
else
_miss "Linux/GTK4" "-DEL_TARGET_LINUX (gtk4 not found)"
echo " Install: apt install libgtk-4-dev"
echo " or: dnf install gtk4-devel"
echo " or: brew install gtk4"
MISSING=$((MISSING + 1))
fi
# ── SDL2 / Embedded Linux / Pi ────────────────────────────────────────────────
SDL2_OK=0
if pkg-config --exists sdl2 2>/dev/null; then
SDL2_VER=$(pkg-config --modversion sdl2 2>/dev/null)
# Also check for SDL2_ttf (needed for text rendering)
if pkg-config --exists SDL2_ttf 2>/dev/null; then
TTF_VER=$(pkg-config --modversion SDL2_ttf 2>/dev/null)
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER}, SDL2_ttf ${TTF_VER}"
else
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (SDL2_ttf missing — needed for text)"
echo " Install: apt install libsdl2-ttf-dev"
fi
SDL2_OK=1
AVAILABLE=$((AVAILABLE + 1))
elif command -v sdl2-config &>/dev/null; then
SDL2_VER=$(sdl2-config --version 2>/dev/null || echo "")
_ok "SDL2/Embedded" "-DEL_TARGET_SDL2 sdl2 ${SDL2_VER} (via sdl2-config)"
SDL2_OK=1
AVAILABLE=$((AVAILABLE + 1))
fi
if [[ $SDL2_OK -eq 0 ]]; then
_miss "SDL2/Embedded" "-DEL_TARGET_SDL2 (sdl2 not found)"
echo " Install: apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev"
echo " or: brew install sdl2 sdl2_ttf sdl2_image"
MISSING=$((MISSING + 1))
fi
# ── LVGL / Microcontrollers ───────────────────────────────────────────────────
LVGL_OK=0
LVGL_WHERE=""
if [ -f "${SCRIPT_DIR}/lvgl/lvgl.h" ]; then
LVGL_WHERE="./lvgl/lvgl.h"
LVGL_OK=1
elif [ -f "${SCRIPT_DIR}/../lvgl/lvgl.h" ]; then
LVGL_WHERE="adjacent lvgl/"
LVGL_OK=1
elif [ -f "/usr/include/lvgl/lvgl.h" ]; then
LVGL_WHERE="/usr/include/lvgl"
LVGL_OK=1
elif [ -f "/usr/local/include/lvgl/lvgl.h" ]; then
LVGL_WHERE="/usr/local/include/lvgl"
LVGL_OK=1
elif pkg-config --exists lvgl 2>/dev/null; then
LVGL_WHERE="pkg-config ($(pkg-config --modversion lvgl 2>/dev/null))"
LVGL_OK=1
fi
if [[ $LVGL_OK -eq 1 ]]; then
_ok "LVGL/MCU" "-DEL_TARGET_LVGL ${LVGL_WHERE}"
AVAILABLE=$((AVAILABLE + 1))
else
_miss "LVGL/MCU" "-DEL_TARGET_LVGL (lvgl.h not found)"
echo " Install: git clone https://github.com/lvgl/lvgl"
echo " (place lvgl/ next to el-compiler/runtime/)"
MISSING=$((MISSING + 1))
fi
# ── Android / JNI ─────────────────────────────────────────────────────────────
ANDROID_OK=0
ANDROID_WHERE=""
if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "${ANDROID_NDK_HOME}" ]; then
NDK_VER=""
if [ -f "${ANDROID_NDK_HOME}/source.properties" ]; then
NDK_VER=$(grep "Pkg.Revision" "${ANDROID_NDK_HOME}/source.properties" \
2>/dev/null | cut -d= -f2 | tr -d ' ' || echo "")
fi
ANDROID_WHERE="NDK ${NDK_VER:-(version unknown)} at \$ANDROID_NDK_HOME"
ANDROID_OK=1
elif command -v ndk-build &>/dev/null; then
ANDROID_WHERE="ndk-build in PATH"
ANDROID_OK=1
elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/ndk" ]; then
ANDROID_WHERE="NDK via \$ANDROID_HOME/ndk"
ANDROID_OK=1
fi
if [[ $ANDROID_OK -eq 1 ]]; then
_ok "Android/JNI" "-DEL_TARGET_ANDROID ${ANDROID_WHERE}"
AVAILABLE=$((AVAILABLE + 1))
else
_miss "Android/JNI" "-DEL_TARGET_ANDROID (ANDROID_NDK_HOME not set)"
echo " Install: https://developer.android.com/studio/releases/ndk"
echo " Then: export ANDROID_NDK_HOME=/path/to/ndk"
MISSING=$((MISSING + 1))
fi
# ── Windows / Win32 (cross or native) ─────────────────────────────────────────
WIN32_OK=0
WIN32_WHERE=""
if [[ "$(uname)" == MINGW* ]] || [[ "$(uname)" == CYGWIN* ]] || \
[[ "$(uname)" == MSYS* ]]; then
WIN32_WHERE="native Windows ($(uname))"
WIN32_OK=1
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
MINGW_VER=$(x86_64-w64-mingw32-gcc --version 2>/dev/null | head -1 || echo "")
WIN32_WHERE="mingw cross-compiler — ${MINGW_VER}"
WIN32_OK=1
elif command -v i686-w64-mingw32-gcc &>/dev/null; then
WIN32_WHERE="mingw 32-bit cross-compiler"
WIN32_OK=1
fi
if [[ $WIN32_OK -eq 1 ]]; then
_ok "Windows/Win32" "-DEL_TARGET_WIN32 ${WIN32_WHERE}"
AVAILABLE=$((AVAILABLE + 1))
else
_miss "Windows/Win32" "-DEL_TARGET_WIN32 (mingw cross-compiler not found)"
echo " Install: brew install mingw-w64"
echo " or: apt install gcc-mingw-w64"
MISSING=$((MISSING + 1))
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo " ${AVAILABLE} platform(s) available, ${MISSING} unavailable on this machine."
echo ""
if [[ -x "${SCRIPT_DIR}/new-platform" ]]; then
echo " Scaffold a new bridge: ${SCRIPT_DIR}/new-platform <name>"
fi
echo " Bridge contract: ${SCRIPT_DIR}/PLATFORM_BRIDGE_SPEC.md"
echo ""
+949
View File
@@ -0,0 +1,949 @@
/*
* el_android.c Android JNI backend for the el native widget system.
*
* This file implements the Android widget layer that el_seed.c calls through
* to when EL_TARGET_ANDROID is defined. It is the exact Android counterpart
* to el_appkit.m and presents the same C API surface.
*
* Architecture:
* el program (el code)
* __widget_* C builtins in el_seed.c
* el_android_* C-callable functions declared here
* ElBridge static methods in Java via JNI
* android.view.View subclasses on the UI thread
*
* Widget handles: every widget (window root, view, control) is assigned an
* int64_t slot index into view_slots[]. The el program holds these as opaque
* Int values. Slot 0 is never valid (null handle = -1 convention).
*
* Threading: Android requires all UI operations to run on the main (UI) thread.
* Every JNI call that mutates a View is dispatched through
* Activity.runOnUiThread(Runnable) if the current thread is not the UI thread.
* el_android_run_loop is a no-op Android lifecycle is driven by the Activity.
*
* Callback mechanism: when a widget fires an event Java calls
* nativeOnClick / nativeOnChange / nativeOnSubmit
* The C side looks up the registered El function name for that slot, then:
* dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string)
* This matches the __thread_create pattern in el_seed.c exactly.
*
* Compile / link (as part of libelruntime.so):
* Compiled by the Android Gradle NDK build system with -DEL_TARGET_ANDROID.
* Link flags: -landroid -llog -ldl
*
* Java companion: ElBridge.java in the same directory must be compiled into
* the Android application's APK (package com.neuron.el).
*/
#ifdef EL_TARGET_ANDROID
#include <jni.h>
#include <android/log.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include "el_runtime.h"
/* ── Logging ─────────────────────────────────────────────────────────────── */
#define EL_TAG "ElAndroid"
#define EL_LOGI(...) __android_log_print(ANDROID_LOG_INFO, EL_TAG, __VA_ARGS__)
#define EL_LOGW(...) __android_log_print(ANDROID_LOG_WARN, EL_TAG, __VA_ARGS__)
#define EL_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, EL_TAG, __VA_ARGS__)
/* ── JNI global state ────────────────────────────────────────────────────── */
static JavaVM *g_jvm = NULL;
static jobject g_activity = NULL; /* global ref to Activity */
static jclass g_bridge_class = NULL; /* global ref to ElBridge class */
/* Cached method IDs on ElBridge — filled in el_android_init(). */
static jmethodID g_mid_createLinearLayout = NULL;
static jmethodID g_mid_createFrameLayout = NULL;
static jmethodID g_mid_createScrollView = NULL;
static jmethodID g_mid_createTextView = NULL;
static jmethodID g_mid_createButton = NULL;
static jmethodID g_mid_createEditText = NULL;
static jmethodID g_mid_createImageView = NULL;
static jmethodID g_mid_setContentView = NULL;
static jmethodID g_mid_setTitle = NULL;
static jmethodID g_mid_addChild = NULL;
static jmethodID g_mid_removeChild = NULL;
static jmethodID g_mid_destroyView = NULL;
static jmethodID g_mid_setText = NULL;
static jmethodID g_mid_getText = NULL;
static jmethodID g_mid_setTextColor = NULL;
static jmethodID g_mid_setBackgroundColor = NULL;
static jmethodID g_mid_setFont = NULL;
static jmethodID g_mid_setPadding = NULL;
static jmethodID g_mid_setWidth = NULL;
static jmethodID g_mid_setHeight = NULL;
static jmethodID g_mid_setFlex = NULL;
static jmethodID g_mid_setCornerRadius = NULL;
static jmethodID g_mid_setEnabled = NULL;
static jmethodID g_mid_setVisibility = NULL;
static jmethodID g_mid_setOnClickListener = NULL;
static jmethodID g_mid_setOnChangeListener = NULL;
static jmethodID g_mid_setOnSubmitListener = NULL;
static jmethodID g_mid_runOnUiThread = NULL; /* Activity.runOnUiThread */
/* ── Widget table ─────────────────────────────────────────────────────────── */
#define EL_ANDROID_MAX_WIDGETS 4096
typedef enum {
EL_WIDGET_FREE = 0,
EL_WIDGET_WINDOW = 1,
EL_WIDGET_VSTACK = 2,
EL_WIDGET_HSTACK = 3,
EL_WIDGET_ZSTACK = 4,
EL_WIDGET_SCROLL = 5,
EL_WIDGET_LABEL = 6,
EL_WIDGET_BUTTON = 7,
EL_WIDGET_TEXTFIELD = 8,
EL_WIDGET_TEXTAREA = 9,
EL_WIDGET_IMAGE = 10,
} ElWidgetKind;
typedef struct {
ElWidgetKind kind;
jint slot; /* Java-side slot index (matches C index) */
char *cb_click; /* El function name for click / submit events */
char *cb_change; /* El function name for value-change events */
} ElWidget;
static ElWidget _el_widgets[EL_ANDROID_MAX_WIDGETS];
static int64_t el_widget_alloc(ElWidgetKind kind, jint slot) {
for (int i = 1; i < EL_ANDROID_MAX_WIDGETS; i++) {
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
_el_widgets[i].kind = kind;
_el_widgets[i].slot = slot;
_el_widgets[i].cb_click = NULL;
_el_widgets[i].cb_change = NULL;
return (int64_t)i;
}
}
EL_LOGE("el_widget_alloc: slot table full");
return -1;
}
static ElWidget *el_widget_get(int64_t handle) {
if (handle <= 0 || handle >= EL_ANDROID_MAX_WIDGETS) return NULL;
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
return &_el_widgets[handle];
}
static void el_widget_free(int64_t handle) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
w->kind = EL_WIDGET_FREE;
w->slot = -1;
free(w->cb_click); w->cb_click = NULL;
free(w->cb_change); w->cb_change = NULL;
}
/* ── JNI environment helpers ─────────────────────────────────────────────── */
/*
* Obtain a JNIEnv for the calling thread. Attaches the thread to the JVM if
* needed (detaches in el_jni_detach_if_attached call in pairs).
*/
static int g_was_attached = 0; /* thread-local would be cleaner but this is
safe for single-threaded el programs */
static JNIEnv *el_jni_env(void) {
if (!g_jvm) return NULL;
JNIEnv *env = NULL;
jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6);
if (rc == JNI_OK) { g_was_attached = 0; return env; }
if (rc == JNI_EDETACHED) {
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) {
g_was_attached = 1;
return env;
}
}
EL_LOGE("el_jni_env: failed to obtain JNIEnv");
return NULL;
}
static void el_jni_detach_if_attached(void) {
if (g_was_attached && g_jvm) {
(*g_jvm)->DetachCurrentThread(g_jvm);
g_was_attached = 0;
}
}
/* ── UI-thread dispatch ──────────────────────────────────────────────────── */
/*
* Most ElBridge static methods already dispatch to the UI thread internally
* (they call Activity.runOnUiThread). The helper below is available for
* cases where the caller needs to be sure the call has completed before
* returning (ElBridge methods marked "sync" use a CountDownLatch internally).
*
* For the current implementation we call ElBridge methods directly; ElBridge
* itself marshals to the UI thread via Activity.runOnUiThread + latch.
* This keeps the C side simple and mirrors the AppKit dispatch_sync pattern.
*/
/* ── JNI_OnLoad ──────────────────────────────────────────────────────────── */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
(void)reserved;
g_jvm = vm;
EL_LOGI("JNI_OnLoad: el Android bridge loaded");
return JNI_VERSION_1_6;
}
/* ── el_android_init ─────────────────────────────────────────────────────── */
/*
* Called from __native_init(). The Activity must have already called
* ElBridge.registerActivity(activity) from Java before this runs, which sets
* g_activity via the nativeRegisterActivity JNI method below.
*
* Caches all method IDs used later so individual widget calls avoid repeated
* FindClass / GetStaticMethodID lookups.
*/
void el_android_init(void) {
static int done = 0;
if (done) return;
done = 1;
JNIEnv *env = el_jni_env();
if (!env) { EL_LOGE("el_android_init: no JNIEnv"); return; }
jclass cls = (*env)->FindClass(env, "com/neuron/el/ElBridge");
if (!cls) { EL_LOGE("el_android_init: ElBridge class not found"); return; }
g_bridge_class = (*env)->NewGlobalRef(env, cls);
(*env)->DeleteLocalRef(env, cls);
#define CACHE_STATIC(var, name, sig) \
var = (*env)->GetStaticMethodID(env, g_bridge_class, name, sig); \
if (!var) EL_LOGW("el_android_init: method not found: %s %s", name, sig)
CACHE_STATIC(g_mid_createLinearLayout, "createLinearLayout", "(II)I");
CACHE_STATIC(g_mid_createFrameLayout, "createFrameLayout", "()I");
CACHE_STATIC(g_mid_createScrollView, "createScrollView", "()I");
CACHE_STATIC(g_mid_createTextView, "createTextView", "(Ljava/lang/String;)I");
CACHE_STATIC(g_mid_createButton, "createButton", "(Ljava/lang/String;)I");
CACHE_STATIC(g_mid_createEditText, "createEditText", "(Ljava/lang/String;Z)I");
CACHE_STATIC(g_mid_createImageView, "createImageView", "(Ljava/lang/String;)I");
CACHE_STATIC(g_mid_setContentView, "setContentView", "(I)V");
CACHE_STATIC(g_mid_setTitle, "setTitle", "(Ljava/lang/String;)V");
CACHE_STATIC(g_mid_addChild, "addChild", "(II)V");
CACHE_STATIC(g_mid_removeChild, "removeChild", "(II)V");
CACHE_STATIC(g_mid_destroyView, "destroyView", "(I)V");
CACHE_STATIC(g_mid_setText, "setText", "(ILjava/lang/String;)V");
CACHE_STATIC(g_mid_getText, "getText", "(I)Ljava/lang/String;");
CACHE_STATIC(g_mid_setTextColor, "setTextColor", "(IFFFF)V");
CACHE_STATIC(g_mid_setBackgroundColor, "setBackgroundColor", "(IFFFF)V");
CACHE_STATIC(g_mid_setFont, "setFont", "(ILjava/lang/String;IZ)V");
CACHE_STATIC(g_mid_setPadding, "setPadding", "(IIIII)V");
CACHE_STATIC(g_mid_setWidth, "setWidth", "(II)V");
CACHE_STATIC(g_mid_setHeight, "setHeight", "(II)V");
CACHE_STATIC(g_mid_setFlex, "setFlex", "(II)V");
CACHE_STATIC(g_mid_setCornerRadius, "setCornerRadius", "(IF)V");
CACHE_STATIC(g_mid_setEnabled, "setEnabled", "(IZ)V");
CACHE_STATIC(g_mid_setVisibility, "setVisibility", "(IZ)V");
CACHE_STATIC(g_mid_setOnClickListener, "setOnClickListener", "(I)V");
CACHE_STATIC(g_mid_setOnChangeListener, "setOnChangeListener", "(I)V");
CACHE_STATIC(g_mid_setOnSubmitListener, "setOnSubmitListener", "(I)V");
#undef CACHE_STATIC
el_jni_detach_if_attached();
EL_LOGI("el_android_init: complete");
}
/* ── JNI: Activity registration ─────────────────────────────────────────── */
/*
* Called from Java: ElBridge.registerActivity(activity) calls back here.
* Stores a global reference to the Activity so C code can dispatch to it.
*/
JNIEXPORT void JNICALL
Java_com_neuron_el_ElBridge_nativeRegisterActivity(JNIEnv *env, jclass cls,
jobject activity) {
(void)cls;
if (g_activity) {
(*env)->DeleteGlobalRef(env, g_activity);
g_activity = NULL;
}
if (activity) {
g_activity = (*env)->NewGlobalRef(env, activity);
EL_LOGI("nativeRegisterActivity: activity registered");
}
}
/* ── El callback invocation ──────────────────────────────────────────────── */
/*
* Invoke an El callback by symbol name.
* Signature matches AppKit: fn(handle: Int, data: String) -> Void
* compiled to: void fn(el_val_t handle, el_val_t data)
*/
typedef void (*ElCb2)(int64_t handle, int64_t data);
static void el_android_invoke_cb(const char *fn_name, int64_t handle,
const char *data) {
if (!fn_name || !*fn_name) return;
void *sym = dlsym(RTLD_DEFAULT, fn_name);
if (!sym) { EL_LOGW("invoke_cb: symbol not found: %s", fn_name); return; }
ElCb2 fn = (ElCb2)sym;
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
}
/* ── JNI: callbacks from Java → C ───────────────────────────────────────── */
JNIEXPORT void JNICALL
Java_com_neuron_el_ElBridge_nativeOnClick(JNIEnv *env, jclass cls, jint slot) {
(void)env; (void)cls;
int64_t handle = (int64_t)slot;
ElWidget *w = el_widget_get(handle);
if (w && w->cb_click) {
el_android_invoke_cb(w->cb_click, handle, "");
}
}
JNIEXPORT void JNICALL
Java_com_neuron_el_ElBridge_nativeOnChange(JNIEnv *env, jclass cls,
jint slot, jstring text) {
(void)cls;
int64_t handle = (int64_t)slot;
ElWidget *w = el_widget_get(handle);
if (w && w->cb_change) {
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
el_android_invoke_cb(w->cb_change, handle, ctext);
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
}
}
JNIEXPORT void JNICALL
Java_com_neuron_el_ElBridge_nativeOnSubmit(JNIEnv *env, jclass cls,
jint slot, jstring text) {
(void)cls;
int64_t handle = (int64_t)slot;
ElWidget *w = el_widget_get(handle);
if (w && w->cb_click) { /* submit stored in cb_click, same as AppKit */
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
el_android_invoke_cb(w->cb_click, handle, ctext);
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
}
}
/* ── Helper: jstring from C string ──────────────────────────────────────── */
static jstring el_jstr(JNIEnv *env, const char *s) {
return (*env)->NewStringUTF(env, s ? s : "");
}
/* ── Window ──────────────────────────────────────────────────────────────── */
/*
* el_android_window_create on Android a "window" is the root LinearLayout
* set as the Activity's content view. We create a vertical LinearLayout and
* store it. el_android_window_show calls setContentView on the Activity.
*/
int64_t el_android_window_create(const char *title, int width, int height,
int min_width, int min_height) {
(void)width; (void)height; (void)min_width; (void)min_height;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
/* VERTICAL LinearLayout with no spacing (spacing added via margins in Java) */
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createLinearLayout,
(jint)1 /* VERTICAL */, (jint)0);
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1;
}
/* Set activity title */
if (g_mid_setTitle && title) {
jstring jtitle = el_jstr(env, title);
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
(*env)->DeleteLocalRef(env, jtitle);
}
int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, (int)slot);
el_jni_detach_if_attached();
return handle;
}
void el_android_window_show(int64_t handle) {
ElWidget *w = el_widget_get(handle);
if (!w || w->kind != EL_WIDGET_WINDOW) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setContentView,
(jint)w->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_window_set_title(int64_t handle, const char *title) {
(void)handle;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
jstring jtitle = el_jstr(env, title);
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
(*env)->DeleteLocalRef(env, jtitle);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
/* ── Layout containers ───────────────────────────────────────────────────── */
int64_t el_android_vstack_create(int spacing) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createLinearLayout,
(jint)1 /* VERTICAL */, (jint)spacing);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_VSTACK, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_hstack_create(int spacing) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createLinearLayout,
(jint)0 /* HORIZONTAL */, (jint)spacing);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_HSTACK, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_zstack_create(void) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createFrameLayout);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_ZSTACK, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_scroll_create(void) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createScrollView);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_SCROLL, (int)slot);
el_jni_detach_if_attached();
return h;
}
/* ── Widget factories ─────────────────────────────────────────────────────── */
int64_t el_android_label_create(const char *text) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jstring jt = el_jstr(env, text);
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createTextView, jt);
(*env)->DeleteLocalRef(env, jt);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_LABEL, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_button_create(const char *label) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jstring jl = el_jstr(env, label);
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createButton, jl);
(*env)->DeleteLocalRef(env, jl);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_text_field_create(const char *placeholder) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jstring jp = el_jstr(env, placeholder);
/* singleLine = true */
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createEditText, jp, (jboolean)JNI_TRUE);
(*env)->DeleteLocalRef(env, jp);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_text_area_create(const char *placeholder) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jstring jp = el_jstr(env, placeholder);
/* singleLine = false → multiline EditText */
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createEditText, jp, (jboolean)JNI_FALSE);
(*env)->DeleteLocalRef(env, jp);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, (int)slot);
el_jni_detach_if_attached();
return h;
}
int64_t el_android_image_create(const char *path) {
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return -1;
jstring jp = el_jstr(env, path);
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
g_mid_createImageView, jp);
(*env)->DeleteLocalRef(env, jp);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
int64_t h = el_widget_alloc(EL_WIDGET_IMAGE, (int)slot);
el_jni_detach_if_attached();
return h;
}
/* ── Widget property setters ─────────────────────────────────────────────── */
void el_android_widget_set_text(int64_t handle, const char *text) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
jstring jt = el_jstr(env, text);
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setText,
(jint)w->slot, jt);
(*env)->DeleteLocalRef(env, jt);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
const char *el_android_widget_get_text(int64_t handle) {
ElWidget *w = el_widget_get(handle);
if (!w) return "";
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return "";
jstring js = (jstring)(*env)->CallStaticObjectMethod(env, g_bridge_class,
g_mid_getText,
(jint)w->slot);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return ""; }
const char *result = "";
if (js) {
const char *cstr = (*env)->GetStringUTFChars(env, js, NULL);
result = cstr ? strdup(cstr) : "";
if (cstr) (*env)->ReleaseStringUTFChars(env, js, cstr);
(*env)->DeleteLocalRef(env, js);
}
el_jni_detach_if_attached();
return result;
}
void el_android_widget_set_color(int64_t handle, float r, float g, float b, float a) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTextColor,
(jint)w->slot, (jfloat)r, (jfloat)g,
(jfloat)b, (jfloat)a);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setBackgroundColor,
(jint)w->slot, (jfloat)r, (jfloat)g,
(jfloat)b, (jfloat)a);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_font(int64_t handle, const char *family, int size, int bold) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
jstring jfam = el_jstr(env, family);
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFont,
(jint)w->slot, jfam, (jint)size,
(jboolean)(bold ? JNI_TRUE : JNI_FALSE));
(*env)->DeleteLocalRef(env, jfam);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setPadding,
(jint)w->slot, (jint)top, (jint)right,
(jint)bottom, (jint)left);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_width(int64_t handle, int width) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setWidth,
(jint)w->slot, (jint)width);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_height(int64_t handle, int height) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setHeight,
(jint)w->slot, (jint)height);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_flex(int64_t handle, int flex) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFlex,
(jint)w->slot, (jint)flex);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_corner_radius(int64_t handle, int radius) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setCornerRadius,
(jint)w->slot, (jfloat)radius);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_disabled(int64_t handle, int disabled) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setEnabled,
(jint)w->slot,
(jboolean)(disabled ? JNI_FALSE : JNI_TRUE));
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_set_hidden(int64_t handle, int hidden) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
/* visible=true means NOT hidden */
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setVisibility,
(jint)w->slot,
(jboolean)(hidden ? JNI_FALSE : JNI_TRUE));
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
/* ── Child management ─────────────────────────────────────────────────────── */
void el_android_widget_add_child(int64_t parent, int64_t child) {
ElWidget *pw = el_widget_get(parent);
ElWidget *cw = el_widget_get(child);
if (!pw || !cw) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_addChild,
(jint)pw->slot, (jint)cw->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_remove_child(int64_t parent, int64_t child) {
ElWidget *pw = el_widget_get(parent);
ElWidget *cw = el_widget_get(child);
if (!pw || !cw) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_removeChild,
(jint)pw->slot, (jint)cw->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
/* ── Event registration ───────────────────────────────────────────────────── */
void el_android_widget_on_click(int64_t handle, const char *fn_name) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
free(w->cb_click);
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
if (!w->cb_click) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnClickListener,
(jint)w->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_on_change(int64_t handle, const char *fn_name) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
free(w->cb_change);
w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
if (!w->cb_change) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnChangeListener,
(jint)w->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
void el_android_widget_on_submit(int64_t handle, const char *fn_name) {
/* Submit stored in cb_click, same as AppKit. */
ElWidget *w = el_widget_get(handle);
if (!w) return;
free(w->cb_click);
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
if (!w->cb_click) return;
JNIEnv *env = el_jni_env();
if (!env || !g_bridge_class) return;
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnSubmitListener,
(jint)w->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
el_jni_detach_if_attached();
}
/* ── Widget destroy ───────────────────────────────────────────────────────── */
void el_android_widget_destroy(int64_t handle) {
ElWidget *w = el_widget_get(handle);
if (!w) return;
JNIEnv *env = el_jni_env();
if (env && g_bridge_class) {
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_destroyView,
(jint)w->slot);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
}
el_widget_free(handle);
el_jni_detach_if_attached();
}
/* ── Manifest reader ─────────────────────────────────────────────────────── */
/*
* __manifest_read: parse the app{} block from a manifest file.
* Returns the raw file contents as an el_val_t (const char* cast).
* The caller (el program) parses the returned string.
* Reads from the filesystem; for APK assets use the AssetManager path instead.
*/
static char *el_read_file(const char *path) {
if (!path || !*path) return NULL;
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
if (len <= 0) { fclose(f); return NULL; }
char *buf = (char *)malloc((size_t)len + 1);
if (!buf) { fclose(f); return NULL; }
fread(buf, 1, (size_t)len, f);
buf[len] = '\0';
fclose(f);
return buf;
}
el_val_t el_android_manifest_read(const char *path) {
char *contents = el_read_file(path);
if (!contents) return (el_val_t)(uintptr_t)"";
return (el_val_t)(uintptr_t)contents; /* caller owns allocation */
}
/* ── __widget_* C API (called from el_seed.c) ────────────────────────────── */
/*
* These are the functions declared in el_native_target.h under EL_TARGET_ANDROID.
* They forward to the el_android_* internal functions above.
*
* The el_val_t / int64_t ABI matches the AppKit functions exactly:
* - Integer params passed as int64_t, extracted with (int)
* - String params passed as int64_t, extracted with (const char*)(uintptr_t)
* - Float params (r,g,b,a) passed as int64_t bit-cast from double; extracted
* with el_to_float / bit-cast union
*/
static inline float el_val_to_float(el_val_t v) {
union { double d; int64_t i; } u;
u.i = v;
return (float)u.d;
}
void __native_init(void) {
el_android_init();
}
void __native_run_loop(void) {
/* No-op on Android — lifecycle is driven by the Activity. */
}
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height) {
return (el_val_t)el_android_window_create(
(const char *)(uintptr_t)title,
(int)width, (int)height, (int)min_width, (int)min_height);
}
void __window_show(el_val_t handle) {
el_android_window_show((int64_t)handle);
}
void __window_set_title(el_val_t handle, el_val_t title) {
el_android_window_set_title((int64_t)handle,
(const char *)(uintptr_t)title);
}
el_val_t __vstack_create(el_val_t spacing) {
return (el_val_t)el_android_vstack_create((int)spacing);
}
el_val_t __hstack_create(el_val_t spacing) {
return (el_val_t)el_android_hstack_create((int)spacing);
}
el_val_t __zstack_create(void) {
return (el_val_t)el_android_zstack_create();
}
el_val_t __scroll_create(void) {
return (el_val_t)el_android_scroll_create();
}
el_val_t __label_create(el_val_t text) {
return (el_val_t)el_android_label_create((const char *)(uintptr_t)text);
}
el_val_t __button_create(el_val_t label) {
return (el_val_t)el_android_button_create((const char *)(uintptr_t)label);
}
el_val_t __text_field_create(el_val_t placeholder) {
return (el_val_t)el_android_text_field_create((const char *)(uintptr_t)placeholder);
}
el_val_t __text_area_create(el_val_t placeholder) {
return (el_val_t)el_android_text_area_create((const char *)(uintptr_t)placeholder);
}
el_val_t __image_create(el_val_t path_or_name) {
return (el_val_t)el_android_image_create((const char *)(uintptr_t)path_or_name);
}
void __widget_set_text(el_val_t handle, el_val_t text) {
el_android_widget_set_text((int64_t)handle,
(const char *)(uintptr_t)text);
}
el_val_t __widget_get_text(el_val_t handle) {
return (el_val_t)(uintptr_t)el_android_widget_get_text((int64_t)handle);
}
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a) {
el_android_widget_set_color((int64_t)handle,
el_val_to_float(r), el_val_to_float(g),
el_val_to_float(b), el_val_to_float(a));
}
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a) {
el_android_widget_set_bg_color((int64_t)handle,
el_val_to_float(r), el_val_to_float(g),
el_val_to_float(b), el_val_to_float(a));
}
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold) {
el_android_widget_set_font((int64_t)handle,
(const char *)(uintptr_t)family,
(int)size, (int)bold);
}
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left) {
el_android_widget_set_padding((int64_t)handle,
(int)top, (int)right, (int)bottom, (int)left);
}
void __widget_set_width(el_val_t handle, el_val_t width) {
el_android_widget_set_width((int64_t)handle, (int)width);
}
void __widget_set_height(el_val_t handle, el_val_t height) {
el_android_widget_set_height((int64_t)handle, (int)height);
}
void __widget_set_flex(el_val_t handle, el_val_t flex) {
el_android_widget_set_flex((int64_t)handle, (int)flex);
}
void __widget_set_corner_radius(el_val_t handle, el_val_t radius) {
el_android_widget_set_corner_radius((int64_t)handle, (int)radius);
}
void __widget_set_disabled(el_val_t handle, el_val_t disabled) {
el_android_widget_set_disabled((int64_t)handle, (int)disabled);
}
void __widget_set_hidden(el_val_t handle, el_val_t hidden) {
el_android_widget_set_hidden((int64_t)handle, (int)hidden);
}
void __widget_add_child(el_val_t parent, el_val_t child) {
el_android_widget_add_child((int64_t)parent, (int64_t)child);
}
void __widget_remove_child(el_val_t parent, el_val_t child) {
el_android_widget_remove_child((int64_t)parent, (int64_t)child);
}
void __widget_destroy(el_val_t handle) {
el_android_widget_destroy((int64_t)handle);
}
void __widget_on_click(el_val_t handle, el_val_t fn_name) {
el_android_widget_on_click((int64_t)handle,
(const char *)(uintptr_t)fn_name);
}
void __widget_on_change(el_val_t handle, el_val_t fn_name) {
el_android_widget_on_change((int64_t)handle,
(const char *)(uintptr_t)fn_name);
}
void __widget_on_submit(el_val_t handle, el_val_t fn_name) {
el_android_widget_on_submit((int64_t)handle,
(const char *)(uintptr_t)fn_name);
}
el_val_t __manifest_read(el_val_t path) {
return el_android_manifest_read((const char *)(uintptr_t)path);
}
#endif /* EL_TARGET_ANDROID */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+844
View File
@@ -0,0 +1,844 @@
/*
* el_lvgl.c LVGL v9 backend for the el native widget system.
*
* This file implements the microcontroller/embedded widget layer that el_seed.c
* calls through to when EL_TARGET_LVGL is defined.
*
* Architecture:
* el program (el code)
* __widget_* C builtins in el_seed.c
* el_lvgl_* C functions defined here
* lv_obj_t widgets via LVGL v9
*
* Target platforms: ESP32, STM32, industrial panels, any system with 256KB+
* RAM and an LVGL-compatible display driver. No OS required.
*
* Widget handles: every widget is assigned an int64_t slot index into
* g_widgets[]. The el program holds these as opaque Int values.
* Slot 0 is reserved; -1 = invalid handle.
*
* Window model: on embedded there is one screen. __window_create configures
* lv_scr_act() as the root container. __window_show is a no-op (the screen
* is always visible). __native_run_loop calls lv_task_handler() in a tight
* loop on RTOS this runs inside a dedicated task; on bare metal it IS the
* main loop. The host application is responsible for initialising the display
* driver and calling lv_tick_inc() before calling __native_run_loop.
*
* Callback dispatch:
* When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing):
* dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime.
* When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32):
* The caller must provide:
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
* which maps function names to function pointers via a compile-time table.
*
* Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font
* maps the requested point size to the nearest available Montserrat variant.
* Bold is approximated by stepping up two sizes (no separate bold face in the
* default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select().
*
* Compile (hosted test build):
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
* # Then link with lvgl.a / lvgl source tree.
*
* Compile (bare-metal, no dynamic linker):
* arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \
* -I./lvgl el_lvgl.c -c -o el_lvgl.o
*/
#ifdef EL_TARGET_LVGL
#include "lvgl/lvgl.h"
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#ifndef EL_LVGL_NO_DLSYM
#include <dlfcn.h>
#endif
#include "el_runtime.h"
/* ── Callback dispatch macro ─────────────────────────────────────────────── */
#ifdef EL_LVGL_NO_DLSYM
/*
* Bare-metal path. The application provides this function:
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
* It maps string names function pointers, typically via a switch on a hash
* or a sorted table of {name, fn_ptr} pairs generated by elc.
*/
extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
#define EL_LVGL_CALL(fn_name, a, b) el_lvgl_dispatch((fn_name), (a), (b))
#else
/*
* Hosted path. dlsym resolves the El symbol at call time.
* We use a compound-statement expression (GCC/Clang extension) to avoid
* executing dlsym more than once per call.
*/
#define EL_LVGL_CALL(fn_name, a, b) \
({ \
typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \
_el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \
_fn ? _fn((a), (b)) : (el_val_t)0; \
})
#endif
/* ── Widget table ─────────────────────────────────────────────────────────── */
#define EL_LVGL_MAX_WIDGETS 4096
/*
* Widget kinds mirrors AppKit/GTK4 backends so future tooling can stay
* consistent across all targets.
*/
typedef enum {
EL_LVGL_FREE = 0,
EL_LVGL_WINDOW = 1,
EL_LVGL_VSTACK = 2,
EL_LVGL_HSTACK = 3,
EL_LVGL_ZSTACK = 4,
EL_LVGL_SCROLL = 5,
EL_LVGL_LABEL = 6,
EL_LVGL_BUTTON = 7, /* lv_btn_create; inner label at slot_btn_label */
EL_LVGL_TEXTFIELD = 8, /* lv_textarea, one-line */
EL_LVGL_TEXTAREA = 9, /* lv_textarea, multiline */
EL_LVGL_IMAGE = 10,
EL_LVGL_DIVIDER = 11,
EL_LVGL_SPACER = 12,
} ElLvglKind;
/*
* Per-slot state. Callback names are stored inline (256 bytes each) to avoid
* heap allocation on targets with no malloc or fragmented heaps.
*/
typedef struct {
ElLvglKind kind;
lv_obj_t *obj; /* primary LVGL object */
lv_obj_t *btn_label; /* for EL_LVGL_BUTTON: inner lv_label child */
char cb_click[256];
char cb_change[256];
char cb_submit[256];
} ElLvglWidget;
static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS];
/* ── Slot helpers ─────────────────────────────────────────────────────────── */
static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) {
for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) {
if (g_widgets[i].kind == EL_LVGL_FREE) {
g_widgets[i].kind = kind;
g_widgets[i].obj = obj;
g_widgets[i].btn_label = NULL;
g_widgets[i].cb_click[0] = '\0';
g_widgets[i].cb_change[0] = '\0';
g_widgets[i].cb_submit[0] = '\0';
return (int64_t)i;
}
}
return -1; /* table full */
}
static ElLvglWidget *lvgl_slot_get(int64_t handle) {
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL;
if (g_widgets[handle].kind == EL_LVGL_FREE) return NULL;
return &g_widgets[handle];
}
static void lvgl_slot_free(int64_t handle) {
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return;
ElLvglWidget *w = &g_widgets[handle];
w->kind = EL_LVGL_FREE;
w->obj = NULL;
w->btn_label = NULL;
w->cb_click[0] = '\0';
w->cb_change[0] = '\0';
w->cb_submit[0] = '\0';
}
/* ── Font selection ───────────────────────────────────────────────────────── */
/*
* LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h):
* 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48
*
* We map the requested size to the nearest available size. Bold is
* approximated by stepping up two sizes (no separate bold face in the default
* font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely.
*/
#ifndef EL_LVGL_CUSTOM_FONT
static const lv_font_t *font_select(int size, int bold) {
/* Step up two sizes for bold approximation. */
if (bold) size += 4;
/* Clamp to available range. */
if (size < 8) size = 8;
if (size > 48) size = 48;
/* Round to nearest even size >= 8. */
if (size % 2 != 0) size++;
switch (size) {
#if LV_FONT_MONTSERRAT_8
case 8: return &lv_font_montserrat_8;
#endif
#if LV_FONT_MONTSERRAT_10
case 10: return &lv_font_montserrat_10;
#endif
#if LV_FONT_MONTSERRAT_12
case 12: return &lv_font_montserrat_12;
#endif
#if LV_FONT_MONTSERRAT_14
case 14: return &lv_font_montserrat_14;
#endif
#if LV_FONT_MONTSERRAT_16
case 16: return &lv_font_montserrat_16;
#endif
#if LV_FONT_MONTSERRAT_18
case 18: return &lv_font_montserrat_18;
#endif
#if LV_FONT_MONTSERRAT_20
case 20: return &lv_font_montserrat_20;
#endif
#if LV_FONT_MONTSERRAT_22
case 22: return &lv_font_montserrat_22;
#endif
#if LV_FONT_MONTSERRAT_24
case 24: return &lv_font_montserrat_24;
#endif
#if LV_FONT_MONTSERRAT_26
case 26: return &lv_font_montserrat_26;
#endif
#if LV_FONT_MONTSERRAT_28
case 28: return &lv_font_montserrat_28;
#endif
#if LV_FONT_MONTSERRAT_30
case 30: return &lv_font_montserrat_30;
#endif
#if LV_FONT_MONTSERRAT_32
case 32: return &lv_font_montserrat_32;
#endif
#if LV_FONT_MONTSERRAT_34
case 34: return &lv_font_montserrat_34;
#endif
#if LV_FONT_MONTSERRAT_36
case 36: return &lv_font_montserrat_36;
#endif
#if LV_FONT_MONTSERRAT_38
case 38: return &lv_font_montserrat_38;
#endif
#if LV_FONT_MONTSERRAT_40
case 40: return &lv_font_montserrat_40;
#endif
#if LV_FONT_MONTSERRAT_42
case 42: return &lv_font_montserrat_42;
#endif
#if LV_FONT_MONTSERRAT_44
case 44: return &lv_font_montserrat_44;
#endif
#if LV_FONT_MONTSERRAT_46
case 46: return &lv_font_montserrat_46;
#endif
#if LV_FONT_MONTSERRAT_48
case 48: return &lv_font_montserrat_48;
#endif
default:
/*
* Requested size is not compiled in. Fall back to the default
* theme font, which is guaranteed to be present.
*/
return LV_FONT_DEFAULT;
}
}
#endif /* EL_LVGL_CUSTOM_FONT */
/* ── Event callback ───────────────────────────────────────────────────────── */
/*
* Single LVGL event callback used for all widget events. The user_data is
* the slot index cast to (void*) via intptr_t avoids heap allocation.
*
* Three event codes are handled:
* LV_EVENT_CLICKED cb_click (buttons, any tappable widget)
* LV_EVENT_VALUE_CHANGED cb_change (textarea, checkbox, etc.)
* LV_EVENT_READY cb_submit (Enter pressed in textarea one-line mode)
*/
static void el_lvgl_event_cb(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
intptr_t slot = (intptr_t)lv_event_get_user_data(e);
ElLvglWidget *w = lvgl_slot_get((int64_t)slot);
if (!w) return;
if (code == LV_EVENT_CLICKED && w->cb_click[0]) {
EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0);
}
if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) {
/*
* Retrieve current text for textarea/textfield widgets so the handler
* receives the updated value as its second argument.
*/
const char *txt = "";
lv_obj_t *target = lv_event_get_target(e);
if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) {
txt = lv_textarea_get_text(target);
if (!txt) txt = "";
}
EL_LVGL_CALL(w->cb_change, (el_val_t)slot,
(el_val_t)(uintptr_t)txt);
}
if (code == LV_EVENT_READY && w->cb_submit[0]) {
/* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */
EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0);
}
}
/* ── Initialisation ───────────────────────────────────────────────────────── */
/*
* el_lvgl_init call lv_init(). The host must have already initialised the
* display driver and input driver before this, or immediately after. Idempotent.
*/
void el_lvgl_init(void) {
static int done = 0;
if (done) return;
done = 1;
lv_init();
}
/*
* el_lvgl_run_loop drive lv_task_handler() indefinitely.
*
* On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task.
* On bare metal: call this as the last statement of main().
*
* The 5 ms delay between handler calls matches the LVGL documentation
* recommendation for a ~200 Hz refresh budget.
*
* On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS
* targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc.
*/
void el_lvgl_run_loop(void) {
for (;;) {
lv_task_handler();
#if defined(EL_LVGL_RTOS_DELAY)
EL_LVGL_RTOS_DELAY(5);
#elif defined(__linux__) || defined(__APPLE__)
{
/* Hosted test build — usleep available. */
#include <unistd.h>
usleep(5000);
}
#endif
/* Bare-metal without a delay macro: the HAL tick increment loop
* is the caller's responsibility. No sleep needed if lv_tick_inc()
* is driven from a hardware timer ISR. */
}
}
/* ── Window ───────────────────────────────────────────────────────────────── */
/*
* el_lvgl_window_create configure lv_scr_act() as a vertical flex container
* and return a slot handle wrapping it. The title is stored for informational
* purposes (e.g., a status bar widget the host might create). Width/height
* are ignored on embedded targets because the screen size is fixed by the
* display driver; they are accepted for API compatibility with other backends.
*/
int64_t el_lvgl_window_create(const char *title, int width, int height,
int min_width, int min_height) {
(void)width; (void)height; (void)min_width; (void)min_height;
lv_obj_t *scr = lv_scr_act();
/* Configure the active screen as a vertical flex container so that
* widgets added via __widget_add_child stack naturally. */
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100));
/* Store the window title in a user-data string on the screen object
* so host code can retrieve it if it wants to render a title bar. */
if (title && *title) {
/* lv_obj_set_user_data stores a void* — we cast the string pointer.
* The string must outlive the screen object; for literals this is
* always true. For dynamic titles use el_lvgl_window_set_title. */
lv_obj_set_user_data(scr, (void *)(uintptr_t)title);
}
/* Allocate a slot for the screen object. */
int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr);
return h;
}
/*
* el_lvgl_window_show no-op on embedded. The screen is always visible.
*/
void el_lvgl_window_show(int64_t handle) {
(void)handle;
/* On multi-screen setups, load the screen: */
/* ElLvglWidget *w = lvgl_slot_get(handle);
* if (w) lv_scr_load(w->obj); */
}
/*
* el_lvgl_window_set_title update the user_data pointer on the screen object.
* On embedded, "title" is typically only used by a custom host title bar.
*/
void el_lvgl_window_set_title(int64_t handle, const char *title) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w || w->kind != EL_LVGL_WINDOW) return;
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : ""));
}
/* ── Layout containers ────────────────────────────────────────────────────── */
/*
* el_lvgl_vstack_create vertical flex column with inter-item gap = spacing.
*/
int64_t el_lvgl_vstack_create(int spacing) {
lv_obj_t *obj = lv_obj_create(lv_scr_act());
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
/* Remove default LVGL border and background so containers are transparent
* by default, matching the AppKit/GTK4 backends. */
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
return lvgl_slot_alloc(EL_LVGL_VSTACK, obj);
}
/*
* el_lvgl_hstack_create horizontal flex row with inter-item gap = spacing.
*/
int64_t el_lvgl_hstack_create(int spacing) {
lv_obj_t *obj = lv_obj_create(lv_scr_act());
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
return lvgl_slot_alloc(EL_LVGL_HSTACK, obj);
}
/*
* el_lvgl_zstack_create plain container, children positioned absolutely.
* No flex flow is set; callers use lv_obj_set_pos() on children directly,
* or rely on their natural 0,0 origin.
*/
int64_t el_lvgl_zstack_create(void) {
lv_obj_t *obj = lv_obj_create(lv_scr_act());
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj);
}
/*
* el_lvgl_scroll_create vertically scrollable container.
*/
int64_t el_lvgl_scroll_create(void) {
lv_obj_t *obj = lv_obj_create(lv_scr_act());
lv_obj_set_scroll_dir(obj, LV_DIR_VER);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
return lvgl_slot_alloc(EL_LVGL_SCROLL, obj);
}
/* ── Widget factories ─────────────────────────────────────────────────────── */
/*
* el_lvgl_label_create static text label.
*/
int64_t el_lvgl_label_create(const char *text) {
lv_obj_t *obj = lv_label_create(lv_scr_act());
lv_label_set_text(obj, text ? text : "");
return lvgl_slot_alloc(EL_LVGL_LABEL, obj);
}
/*
* el_lvgl_button_create pressable button with a child label.
*
* LVGL buttons are containers; text is placed in an inner lv_label child.
* We store the child label pointer in btn_label so set_text / get_text can
* reach it without searching the object tree at runtime.
*/
int64_t el_lvgl_button_create(const char *label) {
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_t *lbl = lv_label_create(btn);
lv_label_set_text(lbl, label ? label : "");
lv_obj_center(lbl);
int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn);
if (h >= 0) {
g_widgets[h].btn_label = lbl;
/* Register click callback immediately so button responds when a
* callback name is registered later via __widget_on_click. */
lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED,
(void *)(intptr_t)h);
}
return h;
}
/*
* el_lvgl_text_field_create single-line text input.
*/
int64_t el_lvgl_text_field_create(const char *placeholder) {
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
lv_textarea_set_one_line(obj, true);
if (placeholder && *placeholder) {
lv_textarea_set_placeholder_text(obj, placeholder);
}
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj);
if (h >= 0) {
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)h);
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY,
(void *)(intptr_t)h);
}
return h;
}
/*
* el_lvgl_text_area_create multi-line text input.
*/
int64_t el_lvgl_text_area_create(const char *placeholder) {
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
lv_textarea_set_one_line(obj, false);
if (placeholder && *placeholder) {
lv_textarea_set_placeholder_text(obj, placeholder);
}
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj);
if (h >= 0) {
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)h);
}
return h;
}
/*
* el_lvgl_image_create image widget.
*
* On hosted Linux (SDL backend), path is a filesystem path.
* On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin".
* LVGL image decoders are registered separately by the host application.
*/
int64_t el_lvgl_image_create(const char *path_or_name) {
lv_obj_t *obj = lv_img_create(lv_scr_act());
if (path_or_name && *path_or_name) {
lv_img_set_src(obj, path_or_name);
}
return lvgl_slot_alloc(EL_LVGL_IMAGE, obj);
}
/* ── Widget property setters ─────────────────────────────────────────────── */
/*
* el_lvgl_widget_set_text update visible text.
*
* Dispatch per kind:
* LABEL lv_label_set_text
* BUTTON lv_label_set_text on inner btn_label child
* TEXTFIELD / TEXTAREA lv_textarea_set_text
* WINDOW lv_obj_set_user_data (stores title string)
*/
void el_lvgl_widget_set_text(int64_t handle, const char *text) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
const char *t = text ? text : "";
switch (w->kind) {
case EL_LVGL_LABEL:
lv_label_set_text(w->obj, t);
break;
case EL_LVGL_BUTTON:
if (w->btn_label) lv_label_set_text(w->btn_label, t);
break;
case EL_LVGL_TEXTFIELD:
case EL_LVGL_TEXTAREA:
lv_textarea_set_text(w->obj, t);
break;
case EL_LVGL_WINDOW:
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t);
break;
default:
break;
}
}
/*
* el_lvgl_widget_get_text retrieve visible text.
*
* Returns a pointer into LVGL's internal storage valid until the next LVGL
* operation that modifies the widget. Callers that need to hold the value
* across LVGL calls must strdup() it.
*/
const char *el_lvgl_widget_get_text(int64_t handle) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return "";
switch (w->kind) {
case EL_LVGL_LABEL:
return lv_label_get_text(w->obj);
case EL_LVGL_BUTTON:
return w->btn_label ? lv_label_get_text(w->btn_label) : "";
case EL_LVGL_TEXTFIELD:
case EL_LVGL_TEXTAREA:
return lv_textarea_get_text(w->obj);
default:
return "";
}
}
/*
* el_lvgl_widget_set_color foreground (text) colour.
*
* r/g/b are 0.01.0 floats bit-cast as el_val_t (see el_runtime.h).
* LVGL lv_color_make takes uint8_t 0255 components.
*/
void el_lvgl_widget_set_color(int64_t handle,
float r, float g, float b, float a) {
(void)a; /* LVGL text colour has no per-glyph alpha channel */
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_color_t c = lv_color_make(
(uint8_t)(r * 255.0f + 0.5f),
(uint8_t)(g * 255.0f + 0.5f),
(uint8_t)(b * 255.0f + 0.5f));
lv_obj_set_style_text_color(w->obj, c, 0);
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
lv_obj_set_style_text_color(w->btn_label, c, 0);
}
}
/*
* el_lvgl_widget_set_bg_color background fill colour + opacity.
*/
void el_lvgl_widget_set_bg_color(int64_t handle,
float r, float g, float b, float a) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_color_t c = lv_color_make(
(uint8_t)(r * 255.0f + 0.5f),
(uint8_t)(g * 255.0f + 0.5f),
(uint8_t)(b * 255.0f + 0.5f));
lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f);
lv_obj_set_style_bg_color(w->obj, c, 0);
lv_obj_set_style_bg_opa(w->obj, opa, 0);
}
/*
* el_lvgl_widget_set_font apply font to text-bearing widget.
*
* The `family` parameter is accepted for API compatibility but LVGL uses
* compiled-in fonts only. Only the size and bold flag have effect unless
* EL_LVGL_CUSTOM_FONT is defined by the host.
*/
void el_lvgl_widget_set_font(int64_t handle,
const char *family, int size, int bold) {
(void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
const lv_font_t *font = font_select(size, bold);
if (!font) return;
lv_obj_set_style_text_font(w->obj, font, 0);
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
lv_obj_set_style_text_font(w->btn_label, font, 0);
}
}
/*
* el_lvgl_widget_set_padding set per-side padding (top/right/bottom/left).
*/
void el_lvgl_widget_set_padding(int64_t handle,
int top, int right, int bottom, int left) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_obj_set_style_pad_top(w->obj, (lv_coord_t)top, 0);
lv_obj_set_style_pad_right(w->obj, (lv_coord_t)right, 0);
lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0);
lv_obj_set_style_pad_left(w->obj, (lv_coord_t)left, 0);
}
/*
* el_lvgl_widget_set_width set explicit pixel width.
*/
void el_lvgl_widget_set_width(int64_t handle, int width) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_obj_set_width(w->obj, (lv_coord_t)width);
}
/*
* el_lvgl_widget_set_height set explicit pixel height.
*/
void el_lvgl_widget_set_height(int64_t handle, int height) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_obj_set_height(w->obj, (lv_coord_t)height);
}
/*
* el_lvgl_widget_set_flex set flex grow factor.
*
* flex > 0 lv_obj_set_flex_grow(obj, flex): object expands to fill
* remaining space proportional to its grow factor.
* flex == 0 lv_obj_set_flex_grow(obj, 0): object uses natural size.
*/
void el_lvgl_widget_set_flex(int64_t handle, int flex) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0));
}
/*
* el_lvgl_widget_set_corner_radius set border radius.
*/
void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0);
}
/*
* el_lvgl_widget_set_disabled enable/disable interactive state.
*
* LV_STATE_DISABLED greys out the widget and prevents input events.
*/
void el_lvgl_widget_set_disabled(int64_t handle, int disabled) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
if (disabled) {
lv_obj_add_state(w->obj, LV_STATE_DISABLED);
} else {
lv_obj_clear_state(w->obj, LV_STATE_DISABLED);
}
}
/*
* el_lvgl_widget_set_hidden show/hide widget.
*
* LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow.
*/
void el_lvgl_widget_set_hidden(int64_t handle, int hidden) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
if (hidden) {
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
}
}
/* ── Tree operations ──────────────────────────────────────────────────────── */
/*
* el_lvgl_widget_add_child attach child widget to parent.
*
* lv_obj_set_parent() reparents the child object inside the LVGL tree.
* For WINDOW parents we use the screen object itself as the parent, since
* lv_scr_act() IS the root container.
*/
void el_lvgl_widget_add_child(int64_t parent, int64_t child) {
ElLvglWidget *pw = lvgl_slot_get(parent);
ElLvglWidget *cw = lvgl_slot_get(child);
if (!pw || !cw) return;
lv_obj_set_parent(cw->obj, pw->obj);
}
/*
* el_lvgl_widget_remove_child detach child from its current parent.
*
* LVGL has no explicit "remove from parent without deleting" operation.
* We reparent the child back to the active screen (making it a root-level
* floating widget) and then hide it. The widget still occupies a slot and
* can be re-attached or destroyed later.
*/
void el_lvgl_widget_remove_child(int64_t parent, int64_t child) {
(void)parent;
ElLvglWidget *cw = lvgl_slot_get(child);
if (!cw) return;
/* Move to screen root and hide. */
lv_obj_set_parent(cw->obj, lv_scr_act());
lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN);
}
/*
* el_lvgl_widget_destroy delete widget and its children from the LVGL tree,
* then free the slot.
*
* lv_obj_del() recursively deletes the object and all children. After this
* call the handle is invalid and must not be used.
*/
void el_lvgl_widget_destroy(int64_t handle) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
lv_obj_del(w->obj);
lvgl_slot_free(handle);
}
/* ── Event registration ───────────────────────────────────────────────────── */
/*
* Event registration stores the El function name in the widget slot. The
* actual lv_obj_add_event_cb() call is made here (or was made in the factory
* for buttons/textfields where we know the relevant event codes upfront).
*
* For widgets that did not register their event callback in the factory (e.g.
* labels receiving a click handler), we add the LVGL event binding now.
*/
void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
strncpy(w->cb_click, fn_name ? fn_name : "", 255);
w->cb_click[255] = '\0';
/*
* Buttons already have LV_EVENT_CLICKED registered in the factory.
* For other widget kinds (labels, containers used as tap targets), add
* the click flag and register the callback.
*/
if (w->kind != EL_LVGL_BUTTON) {
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED,
(void *)(intptr_t)handle);
}
}
void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
strncpy(w->cb_change, fn_name ? fn_name : "", 255);
w->cb_change[255] = '\0';
/*
* Textfield/textarea factories already register VALUE_CHANGED.
* For other kinds (e.g. a custom toggle), add the binding now.
*/
if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) {
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)handle);
}
}
void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) {
ElLvglWidget *w = lvgl_slot_get(handle);
if (!w) return;
strncpy(w->cb_submit, fn_name ? fn_name : "", 255);
w->cb_submit[255] = '\0';
/*
* LV_EVENT_READY fires when Enter is pressed in a one-line textarea.
* Textfield factories already register READY. For other kinds, add it.
*/
if (w->kind != EL_LVGL_TEXTFIELD) {
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY,
(void *)(intptr_t)handle);
}
}
#endif /* EL_TARGET_LVGL */
+574
View File
@@ -0,0 +1,574 @@
/*
* el_native_target.h Native widget declarations for el programs targeting
* native desktop UI (AppKit / GTK4 / Win32).
*
* This header is designed to be included AFTER el_runtime.h without conflict:
* - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h
* static inlines.
* - It does NOT redeclare __println, __print, or other functions whose
* return types differ between el_seed.h and el_runtime.h.
* - It adds: native widget builtins + float arithmetic helpers that the
* current el_runtime.h omits but elc still emits calls to.
*
* Usage:
* Inject via -include at compile time, OR #include it after el_runtime.h.
*
* clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ...
*/
#pragma once
#include <stdint.h>
#include <stdlib.h>
/* el_val_t must already be defined by el_runtime.h or el_seed.h. */
#ifndef EL_VAL_T_DEFINED
typedef int64_t el_val_t;
#endif
/* ── Float arithmetic helpers ───────────────────────────────────────────────
* elc emits calls to float_div / float_mul etc. for Float-typed expressions.
* These were in el_runtime.c through v1.0.0-20260501 but are missing from the
* current el_runtime.h. Redeclared here as static inline to avoid link deps.
* Only defined if not already declared (old runtimes that still have them). */
#ifndef EL_FLOAT_OPS_DEFINED
#define EL_FLOAT_OPS_DEFINED
/* el_to_float / el_from_float — bit-cast between el_val_t and double.
* Defined as static inline in both el_runtime.h and el_seed.h; we do NOT
* redefine them here. We rely on one of those headers being included first. */
static inline el_val_t float_div(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub, ur;
ua.i = a; ub.i = b;
ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0;
return ur.i;
}
static inline el_val_t float_mul(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub, ur;
ua.i = a; ub.i = b; ur.d = ua.d * ub.d;
return ur.i;
}
static inline el_val_t float_add(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub, ur;
ua.i = a; ub.i = b; ur.d = ua.d + ub.d;
return ur.i;
}
static inline el_val_t float_sub(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub, ur;
ua.i = a; ub.i = b; ur.d = ua.d - ub.d;
return ur.i;
}
static inline el_val_t float_lt(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub;
ua.i = a; ub.i = b;
return (el_val_t)(ua.d < ub.d);
}
static inline el_val_t float_gt(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub;
ua.i = a; ub.i = b;
return (el_val_t)(ua.d > ub.d);
}
static inline el_val_t float_lte(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub;
ua.i = a; ub.i = b;
return (el_val_t)(ua.d <= ub.d);
}
static inline el_val_t float_gte(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub;
ua.i = a; ub.i = b;
return (el_val_t)(ua.d >= ub.d);
}
static inline el_val_t float_eq(el_val_t a, el_val_t b) {
union { double d; int64_t i; } ua, ub;
ua.i = a; ub.i = b;
return (el_val_t)(ua.d == ub.d);
}
#endif /* EL_FLOAT_OPS_DEFINED */
/* ── Native widget system (macOS AppKit) ────────────────────────────────────
* Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m.
* Widget handles are opaque int64_t slot indices; -1 = invalid. */
#ifdef EL_TARGET_MACOS
/* Initialisation */
void __native_init(void);
void __native_run_loop(void);
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
void __widget_set_data(el_val_t handle, el_val_t data_str);
/* Manifest reader */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_MACOS */
/* ── Native widget system (Linux GTK4) ──────────────────────────────────────
* Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c.
* Widget handles are opaque int64_t slot indices; -1 = invalid.
* All functions have the same signatures as EL_TARGET_MACOS above. */
#ifdef EL_TARGET_LINUX
/* Initialisation */
void __native_init(void);
void __native_run_loop(void);
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
/* Manifest reader — same JSON output as EL_TARGET_MACOS */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_LINUX */
/* ── Native widget system (Windows Win32) ───────────────────────────────────
* Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c.
* Widget handles are opaque int64_t slot indices; -1 = invalid.
* Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */
#ifdef EL_TARGET_WIN32
/* Initialisation */
void __native_init(void);
void __native_run_loop(void);
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
/* Manifest reader */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_WIN32 */
/* ── Native widget system (iOS UIKit) ───────────────────────────────────────
* Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m.
* Widget handles are opaque int64_t slot indices; -1 = invalid.
*
* iOS lifecycle note: UIApplicationMain never returns. The el program must
* store its UI-build logic in a void(*)(void) function pointer, assign it to
* el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes
* el_main_entry_fn inside didFinishLaunchingWithOptions.
* Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */
#ifdef EL_TARGET_IOS
/* Lifecycle entry-function hook — set before calling __native_run_loop. */
extern void (*el_main_entry_fn)(void);
/* Forward argc/argv from main() to UIApplicationMain. */
void el_uikit_set_args(int argc, char** argv);
/* Initialisation */
void __native_init(void);
void __native_run_loop(void);
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
/* Manifest reader */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_IOS */
/* ── Native widget system (Android JNI) ─────────────────────────────────────
* Available when compiled with -DEL_TARGET_ANDROID and linked with
* libelruntime.so (which includes el_android.c compiled by the NDK build).
* Widget handles are opaque int64_t slot indices; -1 = invalid.
*
* Java companion: ElBridge.java (package com.neuron.el) must be compiled into
* the APK. The Activity must call ElBridge.init(this) before any widget ops.
*
* Link flags (in Android.mk or CMakeLists.txt):
* -landroid -llog -ldl */
#ifdef EL_TARGET_ANDROID
/* Initialisation */
void __native_init(void);
void __native_run_loop(void); /* no-op on Android */
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
/* Manifest reader */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_ANDROID */
/* ── Native widget system (LVGL v9 — embedded / microcontroller) ─────────────
* Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c
* and the LVGL library (lvgl.a or lvgl source tree).
*
* Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+
* RAM and an LVGL-compatible display driver. No OS required.
*
* Widget handles are opaque int64_t slot indices; -1 = invalid.
*
* Bare-metal / no dynamic linker:
* Compile with -DEL_LVGL_NO_DLSYM and provide:
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
*
* Compile:
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
* # Then link with lvgl.a. */
#ifdef EL_TARGET_LVGL
/* Initialisation */
void __native_init(void);
void __native_run_loop(void);
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
/* Manifest reader — same JSON output as all other native targets */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_LVGL */
/* ── Native widget system (SDL2 — embedded / Pi) ────────────────────────────
* Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c.
* Widget handles are opaque int64_t slot indices; -1 = invalid.
*
* Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer
* and SDL2 available. No GTK, no desktop environment required.
*
* Compile:
* gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o
* Link:
* $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */
#ifdef EL_TARGET_SDL2
/* Initialisation */
void __native_init(void);
void __native_run_loop(void);
/* Window */
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
/* Layout containers */
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
/* Widgets */
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
/* Widget properties */
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
/* Layout / tree */
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
/* Events */
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
/* Manifest reader */
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_SDL2 */
+117
View File
@@ -0,0 +1,117 @@
#ifndef EL_PLATFORM_WIN_H
#define EL_PLATFORM_WIN_H
/*
* el_platform_win.h Windows OS-boundary shim for el_runtime.c.
*
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
*
* What it maps:
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
* Sockets close with closesocket() (see el_closesocket), and the stack must be
* started once with WSAStartup done automatically via a load-time constructor.
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
* exported by the main module. Windows equivalent: GetProcAddress on the process
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
* - popen : mapped to _popen/_pclose.
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <io.h>
#include <process.h>
/* Portable headers mingw-w64 provides (verified present). */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
/* ── socket close ─────────────────────────────────────────────────────────── */
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
build defines the same helper as close() so the call sites are identical across platforms. */
static inline int el_closesocket(SOCKET s) { return closesocket(s); }
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
static void el__win_net_init(void) {
static int inited = 0;
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
}
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
#ifndef RTLD_DEFAULT
#define RTLD_DEFAULT ((void*)0)
#endif
static inline void* el_win_dlsym(void* handle, const char* name) {
(void)handle;
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
}
#define dlsym(h, n) el_win_dlsym((h), (n))
/* ── popen / pclose ───────────────────────────────────────────────────────── */
#define popen _popen
#define pclose _pclose
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
#include <direct.h> /* _mkdir */
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
#define timegm _mkgmtime /* UTC tm → time_t */
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
static inline int setenv(const char* name, const char* value, int overwrite) {
(void)overwrite;
return _putenv_s(name, value ? value : "");
}
static inline int unsetenv(const char* name) {
/* _putenv_s(name, "") sets VAR="" rather than removing it.
* SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32
* env block; then we sync the CRT cache with _putenv("NAME="). */
SetEnvironmentVariableA(name, NULL);
size_t len = strlen(name);
char *buf = (char*)malloc(len + 2);
if (!buf) return -1;
memcpy(buf, name, len);
buf[len] = '=';
buf[len + 1] = '\0';
_putenv(buf);
free(buf);
return 0;
}
/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */
static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) {
(void)rem;
DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL));
Sleep(ms ? ms : 1);
return 0;
}
#define nanosleep(req, rem) el_nanosleep((req), (rem))
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
}
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
}
#endif /* EL_PLATFORM_WIN_H */
File diff suppressed because it is too large Load Diff
+14
View File
@@ -52,6 +52,12 @@
typedef int64_t el_val_t;
/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.)
* register handlers across translation units; previously defined only inside el_runtime.c, which
* made cross-module references (and the Windows build) fail. Home in the shared header. */
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
#define EL_INT(v) (v)
@@ -176,6 +182,7 @@ el_val_t http_set_handler(el_val_t name);
* existing handlers (e.g. products/web/server.el): it dispatches with
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
void http_serve_async(el_val_t port, el_val_t handler);
el_val_t http_set_handler_v2(el_val_t name);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
@@ -625,6 +632,7 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
@@ -638,6 +646,12 @@ el_val_t engram_list_layers_json(void);
* no nodes promoted to working memory. */
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
/* ── Working memory ──────────────────────────────────────────────────────────*/
el_val_t engram_wm_count(void);
el_val_t engram_wm_avg_weight(void);
el_val_t engram_wm_top_json(el_val_t n);
el_val_t engram_load_merge(el_val_t path);
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
* All functions call https://api.anthropic.com/v1/messages with the API key
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+711
View File
@@ -0,0 +1,711 @@
#!/usr/bin/env bash
# new-platform — scaffold a new el-native platform bridge
#
# Usage: ./new-platform <platform-name>
#
# Example:
# ./new-platform myplatform
#
# Creates el_myplatform.c with all 33 required __* functions stubbed out,
# the ElWidget slot table, and the dlsym callback dispatcher.
#
# After running, follow the printed instructions to wire the bridge into
# el_native_target.h and el_seed.c.
#
# See PLATFORM_BRIDGE_SPEC.md for the full bridge contract.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Argument validation ───────────────────────────────────────────────────────
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <platform-name>" >&2
echo "" >&2
echo " <platform-name> lowercase identifier, e.g. myrtos, wayland, qt6" >&2
echo "" >&2
echo "This creates el_<platform-name>.c in the current directory." >&2
exit 1
fi
PLATFORM_LOWER="${1,,}" # force lowercase
PLATFORM_UPPER="${PLATFORM_LOWER^^}" # force uppercase
# Validate: letters, digits, underscores only
if [[ ! "${PLATFORM_LOWER}" =~ ^[a-z][a-z0-9_]*$ ]]; then
echo "Error: platform name must start with a letter and contain only a-z, 0-9, _" >&2
exit 1
fi
OUTPUT_FILE="${SCRIPT_DIR}/el_${PLATFORM_LOWER}.c"
if [[ -f "${OUTPUT_FILE}" ]]; then
echo "Error: ${OUTPUT_FILE} already exists." >&2
echo " Remove it first if you want to regenerate it." >&2
exit 1
fi
# ── Generate the bridge file ──────────────────────────────────────────────────
cat > "${OUTPUT_FILE}" << BRIDGE_FILE
/*
* el_${PLATFORM_LOWER}.c — el-native platform bridge for ${PLATFORM_UPPER}.
*
* Generated by new-platform. Replace the TODO stubs with real implementations.
* See PLATFORM_BRIDGE_SPEC.md for the full contract.
*
* ── Slot system ──────────────────────────────────────────────────────────────
* Every widget (window, button, label, container, image) is stored in a static
* array of ElWidget structs. The el program holds an int64_t "handle" which is
* a direct index into that array. Rules:
* - Slot 0 is NEVER valid. Scans start at index 1.
* - Handle -1 means "invalid" / "create failed".
* - Maximum 4096 concurrent widgets.
* - el_widget_get() returns NULL for 0, negative, out-of-range, or FREE slots.
*
* ── Callback ABI ─────────────────────────────────────────────────────────────
* When a platform event fires (click, text change), call el_${PLATFORM_LOWER}_invoke_cb:
*
* el_${PLATFORM_LOWER}_invoke_cb(w->cb_click, slot_index, "");
*
* The El callback signature:
* fn handler(handle: Int, data: String) -> Void
* compiles to:
* void handler(int64_t handle, int64_t data)
* where data is a const char* cast to int64_t (el String representation).
*
* ── Thread safety ────────────────────────────────────────────────────────────
* ALL platform UI calls must run on the main/UI thread. If your platform
* delivers events on background threads (e.g., from a network callback that
* updates a label), marshal to the main thread before calling any widget op.
*
* ── Compile ──────────────────────────────────────────────────────────────────
* cc -DEL_TARGET_${PLATFORM_UPPER} \\
* \$(pkg-config --cflags <your-toolkit>) \\
* -c el_${PLATFORM_LOWER}.c -o el_${PLATFORM_LOWER}.o
*
* ── Link ─────────────────────────────────────────────────────────────────────
* cc el_${PLATFORM_LOWER}.o el_seed.o el_runtime.o -o myapp \\
* \$(pkg-config --libs <your-toolkit>) -ldl -lpthread
*/
#ifdef EL_TARGET_${PLATFORM_UPPER}
/* ── TODO: add platform-specific includes here ──────────────────────────────
* Examples:
* #include <gtk/gtk.h> // GTK4
* #include <SDL2/SDL.h> // SDL2
* #include "lvgl/lvgl.h" // LVGL
* #include <windows.h> // Win32
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <dlfcn.h> /* dlsym — replace with GetProcAddress on Windows */
/* ── Widget table ─────────────────────────────────────────────────────────── */
#define EL_${PLATFORM_UPPER}_MAX_WIDGETS 4096
typedef enum {
EL_WIDGET_FREE = 0,
EL_WIDGET_WINDOW = 1,
EL_WIDGET_VSTACK = 2,
EL_WIDGET_HSTACK = 3,
EL_WIDGET_ZSTACK = 4,
EL_WIDGET_SCROLL = 5,
EL_WIDGET_LABEL = 6,
EL_WIDGET_BUTTON = 7,
EL_WIDGET_TEXTFIELD = 8,
EL_WIDGET_TEXTAREA = 9,
EL_WIDGET_IMAGE = 10,
} ElWidgetKind;
typedef struct {
ElWidgetKind kind;
/* TODO: add your platform's native widget reference here.
* Examples:
* GtkWidget* widget; // GTK4
* SDL_Rect rect; // SDL2 (no object, just geometry)
* lv_obj_t* obj; // LVGL
* HWND hwnd; // Win32
* void* native; // generic pointer
*/
void* native; /* platform widget reference — replace as needed */
/* Text content (cached for platforms that don't provide a get-text API) */
char* text;
/* Foreground / background color (RGBA, 0.0-1.0) */
float fg_r, fg_g, fg_b, fg_a;
float bg_r, bg_g, bg_b, bg_a;
/* Geometry */
int width; /* 0 = not set */
int height; /* 0 = not set */
int flex; /* 0 = hug content, >0 = expand */
/* Padding (top, right, bottom, left) */
int pad_top, pad_right, pad_bottom, pad_left;
/* Corner radius */
int corner_radius;
/* State */
int disabled; /* 0 = enabled, 1 = disabled */
int hidden; /* 0 = visible, 1 = hidden */
/* Event callbacks — El function name resolved at event time via dlsym */
char* cb_click; /* on_click / on_submit */
char* cb_change; /* on_change */
} ElWidget;
static ElWidget _el_widgets[EL_${PLATFORM_UPPER}_MAX_WIDGETS];
/* ── Slot management ──────────────────────────────────────────────────────── */
static int64_t el_widget_alloc(ElWidgetKind kind, void* native) {
for (int i = 1; i < EL_${PLATFORM_UPPER}_MAX_WIDGETS; i++) {
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
memset(&_el_widgets[i], 0, sizeof(ElWidget));
_el_widgets[i].kind = kind;
_el_widgets[i].native = native;
return (int64_t)i;
}
}
fprintf(stderr, "el_${PLATFORM_LOWER}: widget table full (max %d)\n",
EL_${PLATFORM_UPPER}_MAX_WIDGETS);
return -1;
}
static ElWidget* el_widget_get(int64_t handle) {
if (handle <= 0 || handle >= EL_${PLATFORM_UPPER}_MAX_WIDGETS) return NULL;
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
return &_el_widgets[handle];
}
static void el_widget_free(int64_t handle) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* TODO: release w->native (toolkit-specific) */
free(w->text);
free(w->cb_click);
free(w->cb_change);
memset(w, 0, sizeof(ElWidget)); /* sets kind = EL_WIDGET_FREE (0) */
}
/* ── Callback dispatcher ─────────────────────────────────────────────────── */
/*
* Invoke an El function by symbol name. The El function must have the
* compiled signature: void fn(int64_t handle, int64_t data)
* where data is a const char* cast to int64_t (el String representation).
*
* On platforms without dlsym (e.g., Windows), replace with:
* GetProcAddress(GetModuleHandle(NULL), fn_name)
* On embedded targets without dynamic linking, maintain a manual symbol table.
*/
typedef void (*ElCb2)(int64_t handle, int64_t data);
static void el_${PLATFORM_LOWER}_invoke_cb(const char* fn_name,
int64_t handle,
const char* data) {
if (!fn_name || !*fn_name) return;
void* sym = dlsym(RTLD_DEFAULT, fn_name);
if (!sym) {
fprintf(stderr, "el_${PLATFORM_LOWER}: callback symbol not found: %s\n", fn_name);
return;
}
ElCb2 fn = (ElCb2)sym;
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
}
/* ── Lifecycle ────────────────────────────────────────────────────────────── */
/*
* el_${PLATFORM_LOWER}_init — initialize the platform toolkit.
* Must be idempotent (safe to call more than once).
* Called once from __native_init before any widget creation.
*/
void el_${PLATFORM_LOWER}_init(void) {
static int done = 0;
if (done) return;
done = 1;
/* TODO: initialize your platform toolkit here.
* Examples:
* gtk_init(NULL, NULL); // GTK4
* SDL_Init(SDL_INIT_VIDEO); // SDL2
* lv_init(); // LVGL
*/
}
/*
* el_${PLATFORM_LOWER}_run_loop — start the platform event loop.
* On most platforms this NEVER returns. Exceptions: Android (no-op).
* Must be called from the main thread.
*/
void el_${PLATFORM_LOWER}_run_loop(void) {
/* TODO: start the platform event/render loop.
* Examples:
* gtk_main(); // GTK4
* while (1) { SDL_PollEvent(...); render(); SDL_Delay(16); } // SDL2
* while (1) { lv_task_handler(); usleep(5000); } // LVGL
*/
}
/* ── Window ───────────────────────────────────────────────────────────────── */
int64_t el_${PLATFORM_LOWER}_window_create(const char* title, int w, int h,
int mw, int mh) {
/* TODO: create a top-level window.
* title may be NULL — treat as "".
* mw/mh are minimum dimensions (0 = no minimum).
* Return slot handle on success, -1 on failure.
*/
(void)title; (void)w; (void)h; (void)mw; (void)mh;
return -1; /* TODO: implement */
}
void el_${PLATFORM_LOWER}_window_show(int64_t handle) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* TODO: make the window visible. */
}
void el_${PLATFORM_LOWER}_window_set_title(int64_t handle, const char* title) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* TODO: update the window title. title may be NULL — treat as "". */
(void)title;
}
/* ── Layout containers ────────────────────────────────────────────────────── */
int64_t el_${PLATFORM_LOWER}_vstack_create(int spacing) {
/* TODO: create a vertical linear container with the given spacing (px). */
(void)spacing;
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_hstack_create(int spacing) {
/* TODO: create a horizontal linear container with the given spacing (px). */
(void)spacing;
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_zstack_create(void) {
/* TODO: create a z-axis overlay container (children overlap). */
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_scroll_create(void) {
/* TODO: create a scrollable container (vertical scroll, first child = content). */
return -1; /* TODO: implement */
}
/* ── Leaf widgets ─────────────────────────────────────────────────────────── */
int64_t el_${PLATFORM_LOWER}_label_create(const char* text) {
/* TODO: create a non-editable text label. text may be NULL — treat as "". */
(void)text;
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_button_create(const char* label) {
/* TODO: create a clickable button with the given label text.
* Wire up the platform event handler so on_click callbacks fire later. */
(void)label;
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_text_field_create(const char* placeholder) {
/* TODO: create a single-line text input. placeholder may be NULL. */
(void)placeholder;
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_text_area_create(const char* placeholder) {
/* TODO: create a multi-line text input (scrollable). placeholder may be NULL. */
(void)placeholder;
return -1; /* TODO: implement */
}
int64_t el_${PLATFORM_LOWER}_image_create(const char* path_or_name) {
/* TODO: create an image widget.
* Try path_or_name as a filesystem path first, then as a named resource.
* If neither resolves, create an empty image widget (do not crash). */
(void)path_or_name;
return -1; /* TODO: implement */
}
/* ── Widget property setters ─────────────────────────────────────────────── */
void el_${PLATFORM_LOWER}_widget_set_text(int64_t handle, const char* text) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* TODO: update text on label, button, text field, text area, or window title.
* Dispatch on w->kind. text may be NULL — treat as "".
* Cache in w->text if the platform has no get-text API. */
free(w->text);
w->text = strdup(text ? text : "");
}
const char* el_${PLATFORM_LOWER}_widget_get_text(int64_t handle) {
ElWidget* w = el_widget_get(handle);
if (!w) return "";
/* TODO: return the current text of the widget.
* If the platform provides a get-text API, use it.
* Otherwise return w->text (populated by set_text).
* NEVER return NULL — return "" on failure. */
return w->text ? w->text : "";
}
void el_${PLATFORM_LOWER}_widget_set_color(int64_t handle,
float r, float g, float b, float a) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->fg_r = r; w->fg_g = g; w->fg_b = b; w->fg_a = a;
/* TODO: apply foreground (text) color to the platform widget. */
}
void el_${PLATFORM_LOWER}_widget_set_bg_color(int64_t handle,
float r, float g, float b, float a) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->bg_r = r; w->bg_g = g; w->bg_b = b; w->bg_a = a;
/* TODO: apply background color to the platform widget. */
}
void el_${PLATFORM_LOWER}_widget_set_font(int64_t handle,
const char* family, int size, int bold) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* TODO: set font on text-bearing widgets (label, button, text field, text area).
* family may be "system" — use the platform default font in that case.
* Fall back to system font if family is not found. */
(void)family; (void)size; (void)bold;
}
void el_${PLATFORM_LOWER}_widget_set_padding(int64_t handle,
int top, int right,
int bottom, int left) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->pad_top = top; w->pad_right = right;
w->pad_bottom = bottom; w->pad_left = left;
/* TODO: apply padding/insets to the platform container or text widget. */
}
void el_${PLATFORM_LOWER}_widget_set_width(int64_t handle, int width) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->width = width;
/* TODO: apply a fixed-width constraint to the platform widget. */
}
void el_${PLATFORM_LOWER}_widget_set_height(int64_t handle, int height) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->height = height;
/* TODO: apply a fixed-height constraint to the platform widget. */
}
void el_${PLATFORM_LOWER}_widget_set_flex(int64_t handle, int flex) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->flex = flex;
/* TODO: set the flex/expand factor.
* flex > 0 → widget expands to fill available space.
* flex == 0 → widget hugs content size.
* Maps to: content hugging priority (AppKit), hexpand/vexpand (GTK4),
* layout_weight (Android), stretch factor (SDL2 manual layout). */
}
void el_${PLATFORM_LOWER}_widget_set_corner_radius(int64_t handle, int radius) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->corner_radius = radius;
/* TODO: apply rounded corners (requires GPU layer / backing surface on most platforms). */
}
void el_${PLATFORM_LOWER}_widget_set_disabled(int64_t handle, int disabled) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->disabled = disabled;
/* TODO: enable or disable the widget (buttons, text fields). No-op for containers/labels. */
}
void el_${PLATFORM_LOWER}_widget_set_hidden(int64_t handle, int hidden) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
w->hidden = hidden;
/* TODO: show or hide the widget.
* hidden=1 → invisible but still occupies layout space (visibility:hidden semantics).
* For windows: minimize or hide. */
}
/* ── Tree management ─────────────────────────────────────────────────────── */
void el_${PLATFORM_LOWER}_widget_add_child(int64_t parent, int64_t child) {
ElWidget* pw = el_widget_get(parent);
ElWidget* cw = el_widget_get(child);
if (!pw || !cw) return;
/* TODO: attach child to parent. Dispatch on pw->kind:
* EL_WIDGET_WINDOW → add to root content view/container
* EL_WIDGET_VSTACK → add as vertical child
* EL_WIDGET_HSTACK → add as horizontal child
* EL_WIDGET_ZSTACK → add as overlapping subview
* EL_WIDGET_SCROLL → set as the scrollable content view (first child only)
* default → add as plain subview
* Do NOT add a window widget as a child. */
(void)pw; (void)cw;
}
void el_${PLATFORM_LOWER}_widget_remove_child(int64_t parent, int64_t child) {
ElWidget* pw = el_widget_get(parent);
ElWidget* cw = el_widget_get(child);
if (!pw || !cw) return;
/* TODO: detach child from parent. The child slot remains allocated (not destroyed). */
(void)pw; (void)cw;
}
void el_${PLATFORM_LOWER}_widget_destroy(int64_t handle) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
/* TODO: remove the widget from its parent/superview.
* For windows: close the window.
* Free any platform delegate/target objects stored in side tables.
* Then free the slot: */
el_widget_free(handle);
}
/* ── Event registration ───────────────────────────────────────────────────── */
void el_${PLATFORM_LOWER}_widget_on_click(int64_t handle, const char* fn_name) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
free(w->cb_click);
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
}
void el_${PLATFORM_LOWER}_widget_on_change(int64_t handle, const char* fn_name) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
free(w->cb_change);
w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
}
void el_${PLATFORM_LOWER}_widget_on_submit(int64_t handle, const char* fn_name) {
/* Submit (Enter key in text field) reuses the cb_click slot, matching AppKit convention. */
el_${PLATFORM_LOWER}_widget_on_click(handle, fn_name);
}
/* ── Example event handler (adapt to your platform's callback mechanism) ─── */
/*
* When a button is clicked in your platform event loop, call:
*
* ElWidget* w = el_widget_get(slot_index);
* if (w && w->cb_click) {
* el_${PLATFORM_LOWER}_invoke_cb(w->cb_click, slot_index, "");
* }
*
* When a text field changes:
*
* ElWidget* w = el_widget_get(slot_index);
* if (w && w->cb_change) {
* el_${PLATFORM_LOWER}_invoke_cb(w->cb_change, slot_index, current_text);
* }
*/
#endif /* EL_TARGET_${PLATFORM_UPPER} */
BRIDGE_FILE
chmod 644 "${OUTPUT_FILE}"
# ── Print next-steps instructions ────────────────────────────────────────────
UPPER="${PLATFORM_UPPER}"
LOWER="${PLATFORM_LOWER}"
cat << INSTRUCTIONS
Created: el_${LOWER}.c
Next steps:
────────────────────────────────────────────────────────────────────────────
1. Add to el_native_target.h (inside a new #ifdef EL_TARGET_${UPPER} block):
#ifdef EL_TARGET_${UPPER}
void __native_init(void);
void __native_run_loop(void);
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height);
void __window_show(el_val_t handle);
void __window_set_title(el_val_t handle, el_val_t title);
el_val_t __vstack_create(el_val_t spacing);
el_val_t __hstack_create(el_val_t spacing);
el_val_t __zstack_create(void);
el_val_t __scroll_create(void);
el_val_t __label_create(el_val_t text);
el_val_t __button_create(el_val_t label);
el_val_t __text_field_create(el_val_t placeholder);
el_val_t __text_area_create(el_val_t placeholder);
el_val_t __image_create(el_val_t path_or_name);
void __widget_set_text(el_val_t handle, el_val_t text);
el_val_t __widget_get_text(el_val_t handle);
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a);
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold);
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left);
void __widget_set_width(el_val_t handle, el_val_t width);
void __widget_set_height(el_val_t handle, el_val_t height);
void __widget_set_flex(el_val_t handle, el_val_t flex);
void __widget_set_corner_radius(el_val_t handle, el_val_t radius);
void __widget_set_disabled(el_val_t handle, el_val_t disabled);
void __widget_set_hidden(el_val_t handle, el_val_t hidden);
void __widget_add_child(el_val_t parent, el_val_t child);
void __widget_remove_child(el_val_t parent, el_val_t child);
void __widget_destroy(el_val_t handle);
void __widget_on_click(el_val_t handle, el_val_t fn_name);
void __widget_on_change(el_val_t handle, el_val_t fn_name);
void __widget_on_submit(el_val_t handle, el_val_t fn_name);
el_val_t __manifest_read(el_val_t path);
#endif /* EL_TARGET_${UPPER} */
────────────────────────────────────────────────────────────────────────────
2. Add to el_seed.c (copy the EL_TARGET_MACOS block as a template and
substitute el_appkit_ → el_${LOWER}_):
#ifdef EL_TARGET_${UPPER}
/* Forward declarations — implemented in el_${LOWER}.c */
extern void el_${LOWER}_init(void);
extern void el_${LOWER}_run_loop(void);
extern int64_t el_${LOWER}_window_create(const char* title, int w, int h, int mw, int mh);
extern void el_${LOWER}_window_show(int64_t handle);
extern void el_${LOWER}_window_set_title(int64_t handle, const char* title);
extern int64_t el_${LOWER}_vstack_create(int spacing);
extern int64_t el_${LOWER}_hstack_create(int spacing);
extern int64_t el_${LOWER}_zstack_create(void);
extern int64_t el_${LOWER}_scroll_create(void);
extern int64_t el_${LOWER}_label_create(const char* text);
extern int64_t el_${LOWER}_button_create(const char* label);
extern int64_t el_${LOWER}_text_field_create(const char* placeholder);
extern int64_t el_${LOWER}_text_area_create(const char* placeholder);
extern int64_t el_${LOWER}_image_create(const char* path_or_name);
extern void el_${LOWER}_widget_set_text(int64_t handle, const char* text);
extern const char* el_${LOWER}_widget_get_text(int64_t handle);
extern void el_${LOWER}_widget_set_color(int64_t h, float r, float g, float b, float a);
extern void el_${LOWER}_widget_set_bg_color(int64_t h, float r, float g, float b, float a);
extern void el_${LOWER}_widget_set_font(int64_t h, const char* family, int size, int bold);
extern void el_${LOWER}_widget_set_padding(int64_t h, int top, int right, int bottom, int left);
extern void el_${LOWER}_widget_set_width(int64_t h, int width);
extern void el_${LOWER}_widget_set_height(int64_t h, int height);
extern void el_${LOWER}_widget_set_flex(int64_t h, int flex);
extern void el_${LOWER}_widget_set_corner_radius(int64_t h, int radius);
extern void el_${LOWER}_widget_set_disabled(int64_t h, int disabled);
extern void el_${LOWER}_widget_set_hidden(int64_t h, int hidden);
extern void el_${LOWER}_widget_add_child(int64_t parent, int64_t child);
extern void el_${LOWER}_widget_remove_child(int64_t parent, int64_t child);
extern void el_${LOWER}_widget_destroy(int64_t handle);
extern void el_${LOWER}_widget_on_click(int64_t h, const char* fn_name);
extern void el_${LOWER}_widget_on_change(int64_t h, const char* fn_name);
extern void el_${LOWER}_widget_on_submit(int64_t h, const char* fn_name);
void __native_init(void) { el_${LOWER}_init(); }
void __native_run_loop(void) { el_${LOWER}_run_loop(); }
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height) {
return (el_val_t)el_${LOWER}_window_create(
EL_CSTR(title),
(int)(int64_t)width, (int)(int64_t)height,
(int)(int64_t)min_width, (int)(int64_t)min_height);
}
void __window_show(el_val_t h) { el_${LOWER}_window_show((int64_t)h); }
void __window_set_title(el_val_t h, el_val_t t) { el_${LOWER}_window_set_title((int64_t)h, EL_CSTR(t)); }
el_val_t __vstack_create(el_val_t s) { return (el_val_t)el_${LOWER}_vstack_create((int)(int64_t)s); }
el_val_t __hstack_create(el_val_t s) { return (el_val_t)el_${LOWER}_hstack_create((int)(int64_t)s); }
el_val_t __zstack_create(void) { return (el_val_t)el_${LOWER}_zstack_create(); }
el_val_t __scroll_create(void) { return (el_val_t)el_${LOWER}_scroll_create(); }
el_val_t __label_create(el_val_t t) { return (el_val_t)el_${LOWER}_label_create(EL_CSTR(t)); }
el_val_t __button_create(el_val_t l) { return (el_val_t)el_${LOWER}_button_create(EL_CSTR(l)); }
el_val_t __text_field_create(el_val_t p) { return (el_val_t)el_${LOWER}_text_field_create(EL_CSTR(p)); }
el_val_t __text_area_create(el_val_t p) { return (el_val_t)el_${LOWER}_text_area_create(EL_CSTR(p)); }
el_val_t __image_create(el_val_t p) { return (el_val_t)el_${LOWER}_image_create(EL_CSTR(p)); }
void __widget_set_text(el_val_t h, el_val_t t) { el_${LOWER}_widget_set_text((int64_t)h, EL_CSTR(t)); }
el_val_t __widget_get_text(el_val_t h) {
const char* s = el_${LOWER}_widget_get_text((int64_t)h);
return EL_STR(s ? s : "");
}
void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) {
el_${LOWER}_widget_set_color((int64_t)h,
(float)el_to_float(r), (float)el_to_float(g),
(float)el_to_float(b), (float)el_to_float(a));
}
void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) {
el_${LOWER}_widget_set_bg_color((int64_t)h,
(float)el_to_float(r), (float)el_to_float(g),
(float)el_to_float(b), (float)el_to_float(a));
}
void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) {
el_${LOWER}_widget_set_font((int64_t)h, EL_CSTR(family),
(int)(int64_t)size, (int)(int64_t)bold);
}
void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left) {
el_${LOWER}_widget_set_padding((int64_t)h,
(int)(int64_t)top, (int)(int64_t)right,
(int)(int64_t)bottom, (int)(int64_t)left);
}
void __widget_set_width(el_val_t h, el_val_t w) { el_${LOWER}_widget_set_width((int64_t)h, (int)(int64_t)w); }
void __widget_set_height(el_val_t h, el_val_t ht) { el_${LOWER}_widget_set_height((int64_t)h, (int)(int64_t)ht); }
void __widget_set_flex(el_val_t h, el_val_t f) { el_${LOWER}_widget_set_flex((int64_t)h, (int)(int64_t)f); }
void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_${LOWER}_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); }
void __widget_set_disabled(el_val_t h, el_val_t d) { el_${LOWER}_widget_set_disabled((int64_t)h, (int)(int64_t)d); }
void __widget_set_hidden(el_val_t h, el_val_t hid) { el_${LOWER}_widget_set_hidden((int64_t)h, (int)(int64_t)hid); }
void __widget_add_child(el_val_t p, el_val_t c) { el_${LOWER}_widget_add_child((int64_t)p, (int64_t)c); }
void __widget_remove_child(el_val_t p, el_val_t c) { el_${LOWER}_widget_remove_child((int64_t)p, (int64_t)c); }
void __widget_destroy(el_val_t h) { el_${LOWER}_widget_destroy((int64_t)h); }
void __widget_on_click(el_val_t h, el_val_t fn) { el_${LOWER}_widget_on_click((int64_t)h, EL_CSTR(fn)); }
void __widget_on_change(el_val_t h, el_val_t fn) { el_${LOWER}_widget_on_change((int64_t)h, EL_CSTR(fn)); }
void __widget_on_submit(el_val_t h, el_val_t fn) { el_${LOWER}_widget_on_submit((int64_t)h, EL_CSTR(fn)); }
#endif /* EL_TARGET_${UPPER} */
────────────────────────────────────────────────────────────────────────────
3. Implement each TODO in el_${LOWER}.c.
4. Compile:
cc -DEL_TARGET_${UPPER} -Wall \\
\$(pkg-config --cflags <your-toolkit> 2>/dev/null) \\
-c el_${LOWER}.c -o el_${LOWER}.o
5. Link:
cc el_${LOWER}.o el_seed.o el_runtime.o -o myapp \\
\$(pkg-config --libs <your-toolkit> 2>/dev/null) \\
-ldl -lpthread
See PLATFORM_BRIDGE_SPEC.md for the full bridge contract and gotchas.
INSTRUCTIONS
+303 -3
View File
@@ -2916,6 +2916,24 @@ fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
return true
}
// fn_has_decorator does this FnDef carry a decorator named `name`?
// Reads the `decorators` list [{name, args}] attached by the parser. Absent
// key -> native_list_len returns 0 -> false. This is the multi-decorator-aware
// replacement for the old single `decorator` string check, so a fn may stack
// roles with other decorators (e.g. `@route(...) @manager fn ...`).
fn fn_has_decorator(stmt: Map<String, Any>, name: String) -> Bool {
let dl = stmt["decorators"]
let n: Int = native_list_len(dl)
let i = 0
while i < n {
let d = native_list_get(dl, i)
let dn: String = d["name"]
if str_eq(dn, name) { return true }
let i = i + 1
}
false
}
fn cg_fn(stmt: Map<String, Any>) -> Void {
let fn_name: String = stmt["name"]
// Skip El's `fn main()` - C provides its own main() for top-level stmts
@@ -2927,10 +2945,10 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
let params_c: String = params_to_c(params)
// VBD role enforcement: dharma_emit / dharma_field may only be called
// from @manager-decorated functions. Surface violations to the C compiler
// via #error directives emitted before the function definition.
let decorator: String = stmt["decorator"]
// via #error directives emitted before the function definition. Read the
// decorator LIST so the role may be stacked with other decorators.
if vbd_has_restricted_call(body) {
if !str_eq(decorator, "manager") {
if !fn_has_decorator(stmt, "manager") {
emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"")
}
}
@@ -2938,6 +2956,15 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
// arithmetic vs concat on type-annotated identifiers.
build_int_names_for_params(params)
emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
// API-reshape decorator-seam: auto-emit at the decorated-fn boundary
// Every @manager/@accessor fn gets ONE injected call to engram_boundary_beat
// at entry interoception (chrono tick) + telemetry (afferent counter) +
// strengthen (self-activity) + a dharma bus event so a decorated op
// self-reports with ZERO hand-written instrumentation in its body. (VBD role
// = the topmost decorator; write it topmost when stacking with @route.)
if fn_has_decorator(stmt, "manager") || fn_has_decorator(stmt, "accessor") {
emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "));")
}
// Seed declared with parameter names so reassignment works
let decl = native_list_empty()
let np: Int = native_list_len(params)
@@ -3479,6 +3506,259 @@ fn cg_decl_streaming(stmt: Map<String, Any>) -> Void {
}
}
// @route dispatcher generation
//
// Scan the token stream for @route-decorated fns and synthesize a generic HTTP
// dispatcher `el_route_dispatch(method, clean, path, body)`. A decorated handler
// must have the uniform signature (method, path, body) -> String. The dispatcher
// matches `clean` (the query-stripped path, supplied by the caller) against each
// route and calls the handler with the ORIGINAL `path` so query strings survive.
// Returns the sentinel "__EL_NO_ROUTE__" when nothing matches, so the caller may
// fall through to any remaining hand-written branches (mixed mode).
//
// Decorator grammar: @route(path, method, kind, suffix)
// path the match string (or the prefix, for compound)
// method "GET" | "POST" | ... ; a '|'-list like "GET|POST"; "ANY"/"" = no guard
// kind "exact" (default) | "prefix" | "suffix" | "compound"
// suffix for "compound": the required str_ends_with suffix
//
// The dispatch table is emitted SPECIFICITY-SORTED (most-specific first), NOT in
// source order, so overlapping prefixes (e.g. /api/x/search vs /api/x) never
// shadow each other regardless of how the handlers are written.
// split_pipe split "GET|POST" on '|' into ["GET","POST"]. Self-contained
// (no dependency on str_split runtime semantics).
fn split_pipe(s: String) -> [String] {
let out: [String] = native_list_empty()
let cur: String = ""
let n: Int = str_len(s)
let i: Int = 0
while i < n {
let ch: String = str_slice(s, i, i + 1)
if str_eq(ch, "|") {
let out = native_list_append(out, cur)
let cur = ""
} else {
let cur = cur + ch
}
let i = i + 1
}
let out = native_list_append(out, cur)
out
}
// route_make_record build a route record map from the @route decorator args.
fn route_make_record(fn_name: String, args: [String]) -> Map<String, Any> {
let na: Int = native_list_len(args)
let rpath: String = ""
if na >= 1 { let rpath = native_list_get(args, 0) }
let rmethod: String = "GET"
if na >= 2 { let rmethod = native_list_get(args, 1) }
let rkind: String = "exact"
if na >= 3 { let rkind = native_list_get(args, 2) }
let rsuffix: String = ""
if na >= 4 { let rsuffix = native_list_get(args, 3) }
{ "name": fn_name, "path": rpath, "method": rmethod, "kind": rkind, "suffix": rsuffix }
}
// route_spec_score higher = more specific = emitted earlier. Ordering:
// exact > compound > suffix > prefix; within a class, a longer path/suffix
// wins (so /api/x/search sorts before /api/x). Guarantees correct dispatch
// independent of source order.
fn route_spec_score(rec: Map<String, Any>) -> Int {
let kind: String = rec["kind"]
let path: String = rec["path"]
let suffix: String = rec["suffix"]
let plen: Int = str_len(path)
let slen: Int = str_len(suffix)
if str_eq(kind, "exact") { return 4000000 + plen }
if str_eq(kind, "compound") { return 3000000 + plen * 100 + slen }
if str_eq(kind, "suffix") { return 2000000 + slen }
return 1000000 + plen
}
// route_sort_desc selection sort of route records by descending specificity.
// N is small (routes per module), so O(n^2) is fine and keeps codegen simple.
fn route_sort_desc(recs: [Map<String, Any>]) -> [Map<String, Any>] {
let n: Int = native_list_len(recs)
let out: [Map<String, Any>] = native_list_empty()
let used: [Bool] = native_list_empty()
let u: Int = 0
while u < n {
let used = native_list_append(used, false)
let u = u + 1
}
let picked: Int = 0
while picked < n {
let best_i: Int = 0 - 1
let best_score: Int = 0 - 1
let i: Int = 0
while i < n {
let is_used: Bool = native_list_get(used, i)
if !is_used {
let sc: Int = route_spec_score(native_list_get(recs, i))
if sc > best_score {
let best_score = sc
let best_i = i
}
}
let i = i + 1
}
let out = native_list_append(out, native_list_get(recs, best_i))
// Rebuild `used` with best_i marked (runtime has no native_list_set).
let new_used: [Bool] = native_list_empty()
let j: Int = 0
while j < n {
if j == best_i {
let new_used = native_list_append(new_used, true)
} else {
let new_used = native_list_append(new_used, native_list_get(used, j))
}
let j = j + 1
}
let used = new_used
let picked = picked + 1
}
out
}
// scan_routes token-level scan collecting every @route-decorated fn as a
// route record. Runs once per module (like scan_fn_sigs) so the dispatcher can
// be synthesized in the streaming backend, which discards per-fn ASTs. Handles
// decorator STACKING: `@route(...) @manager fn` still records the route.
fn scan_routes(tokens: [Any]) -> [Map<String, Any>] {
let total: Int = native_list_len(tokens) / 2
let recs: [Map<String, Any>] = native_list_empty()
let has_pending: Bool = false
let pending_args: [String] = native_list_empty()
let pos: Int = 0
let going: Bool = true
while going {
if pos >= total {
let going = false
} else {
let k: String = tok_kind(tokens, pos)
if str_eq(k, "Eof") {
let going = false
} else {
if str_eq(k, "At") {
let dname: String = tok_value(tokens, pos + 1)
let p: Int = pos + 2
let args: [String] = native_list_empty()
let ka: String = tok_kind(tokens, p)
if str_eq(ka, "LParen") {
let p = p + 1
let running: Bool = true
while running {
let kd: String = tok_kind(tokens, p)
if str_eq(kd, "RParen") {
let running = false
} else {
if str_eq(kd, "Eof") {
let running = false
} else {
if str_eq(kd, "Str") {
let args = native_list_append(args, tok_value(tokens, p))
}
let p = p + 1
}
}
}
if str_eq(tok_kind(tokens, p), "RParen") { let p = p + 1 }
}
if str_eq(dname, "route") {
let has_pending = true
let pending_args = args
}
let pos = p
} else {
if str_eq(k, "Fn") {
let fname: String = tok_value(tokens, pos + 1)
if has_pending {
let recs = native_list_append(recs, route_make_record(fname, pending_args))
let has_pending = false
}
let pos = pos + 2
} else {
let pos = pos + 1
}
}
}
}
}
recs
}
// program_has_routes did scan_routes find any @route fn?
fn program_has_routes(recs: [Map<String, Any>]) -> Bool {
native_list_len(recs) > 0
}
// route_method_guard C boolean prefix guarding on HTTP method, or "" for none.
fn route_method_guard(method: String) -> String {
if str_eq(method, "") { return "" }
if str_eq(method, "ANY") { return "" }
if str_contains(method, "|") {
let parts: [String] = split_pipe(method)
let np: Int = native_list_len(parts)
let expr: String = ""
let i: Int = 0
while i < np {
let m: String = native_list_get(parts, i)
if str_eq(m, "") {
let i = i + 1
} else {
let piece: String = "str_eq(method, EL_STR(" + c_str_lit(m) + "))"
if str_eq(expr, "") {
let expr = piece
} else {
let expr = expr + " || " + piece
}
let i = i + 1
}
}
if str_eq(expr, "") { return "" }
return "(" + expr + ") && "
}
"str_eq(method, EL_STR(" + c_str_lit(method) + ")) && "
}
// route_match_expr C boolean matching `clean` against the route path/kind.
fn route_match_expr(kind: String, path: String, suffix: String) -> String {
if str_eq(kind, "prefix") {
return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + "))"
}
if str_eq(kind, "suffix") {
return "str_ends_with(clean, EL_STR(" + c_str_lit(path) + "))"
}
if str_eq(kind, "compound") {
return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + ")) && str_ends_with(clean, EL_STR(" + c_str_lit(suffix) + "))"
}
"str_eq(clean, EL_STR(" + c_str_lit(path) + "))"
}
// emit_route_dispatch emit the generated el_route_dispatch definition from the
// specificity-sorted route records. No-op if there are no routes.
fn emit_route_dispatch(recs: [Map<String, Any>]) -> Void {
if !program_has_routes(recs) { return }
let sorted: [Map<String, Any>] = route_sort_desc(recs)
emit_line("// ── generated @route dispatcher (specificity-sorted) ──")
emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body) {")
let n: Int = native_list_len(sorted)
let i: Int = 0
while i < n {
let rec = native_list_get(sorted, i)
let guard: String = route_method_guard(rec["method"])
let match_e: String = route_match_expr(rec["kind"], rec["path"], rec["suffix"])
let fn_name: String = rec["name"]
emit_line(" if (" + guard + match_e + ") { return " + fn_name + "(method, path, body); }")
let i = i + 1
}
emit_line(" return EL_STR(\"__EL_NO_ROUTE__\");")
emit_line("}")
emit_blank()
}
// emit_streaming_preamble emit #includes, forward decls, and file-scope lets
// using the pre-scanned signature data (no full AST).
fn emit_streaming_preamble(sigs: [Map<String, Any>], source: String) -> Void {
@@ -3571,6 +3851,17 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
emit_streaming_preamble(sigs, source)
el_arena_pop(preamble_mark)
// @route: scan the token stream once for @route-decorated fns. Kept in
// codegen_streaming scope (survives the per-fn arena pops and el_release of
// tokens below via refcount, like `sigs`). If any exist, forward-declare the
// generated dispatcher NOW so hand-written fns (e.g. handle_request) may call
// it before its definition is emitted after the fn-emit loop.
let route_records: [Map<String, Any>] = scan_routes(tokens)
if program_has_routes(route_records) {
emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body);")
emit_blank()
}
// Detect whether there is a fn main() and whether there are top-level
// executable stmts (for library detection) from sigs.
let has_el_main: Bool = false
@@ -3758,6 +4049,15 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
}
}
// @route: emit the generated dispatcher definition now after every handler
// fn has been emitted, but before `tokens` is released (route_records holds
// its own refs to the extracted strings). No-op unless the module declared
// at least one @route fn. Emitted before the test/library early-returns so it
// is present in library modules (e.g. neuron's routes.el) too.
let route_arena_mark: Any = el_arena_push()
emit_route_dispatch(route_records)
el_arena_pop(route_arena_mark)
// Tokens fully consumed by the streaming loop release now to free peak heap.
el_release(tokens)
+72 -3
View File
@@ -23,10 +23,29 @@ fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
}
fn tok_kind(tokens: [Any], pos: Int) -> String {
// Out-of-range reads must report the Eof sentinel so every `== "Eof"`
// termination guard in the parser fires. Without this, reading past the
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
// which matches no delimiter, letting inner parse loops append AST nodes
// forever on malformed input -> unbounded allocation -> OOM.
let n: Int = native_list_len(tokens) / 2
if pos < 0 {
return "Eof"
}
if pos >= n {
return "Eof"
}
native_list_get(tokens, pos * 2)
}
fn tok_value(tokens: [Any], pos: Int) -> String {
let n: Int = native_list_len(tokens) / 2
if pos < 0 {
return ""
}
if pos >= n {
return ""
}
native_list_get(tokens, pos * 2 + 1)
}
@@ -35,7 +54,12 @@ fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
if k == kind {
return pos + 1
}
// On mismatch just advance; error recovery is best-effort
// On mismatch, error recovery is best-effort. But never step PAST the Eof
// sentinel: once at Eof a mismatch means the input ended early, and
// advancing would run the cursor off the token list.
if k == "Eof" {
return pos
}
pos + 1
}
@@ -1734,23 +1758,68 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p)
}
// @decorator - capture decorator name and attach to following stmt
// @decorator - capture decorator name (and optional string args) and
// attach to the following stmt. Backward-compatible: bare @manager /
// @engine / @accessor still parse (no parens -> empty args). Decorators
// STACK: `@route("/p","GET") @manager fn f()` attaches BOTH to f via a
// `decorators` list [{name, args}]. The legacy `decorator` string is kept
// populated (topmost decorator) so the JS backend keeps working unchanged.
if k == "At" {
let p = pos + 1
let dec_name = tok_value(tokens, p)
let p = p + 1
// Optional decorator argument list: @name("a", "b", ...)
let dec_args = native_list_empty()
let ka = tok_kind(tokens, p)
if str_eq(ka, "LParen") {
let p = p + 1
let running_da = true
while running_da {
let kd = tok_kind(tokens, p)
if str_eq(kd, "RParen") {
let running_da = false
} else {
if str_eq(kd, "Eof") {
let running_da = false
} else {
if str_eq(kd, "Str") {
let dec_args = native_list_append(dec_args, tok_value(tokens, p))
}
let p = p + 1
let kc = tok_kind(tokens, p)
if str_eq(kc, "Comma") {
let p = p + 1
}
}
}
}
let p = expect(tokens, p, "RParen")
}
let r = parse_stmt(tokens, p)
let inner = r["node"]
let p2 = r["pos"]
let inner_kind: String = inner["stmt"]
if str_eq(inner_kind, "FnDef") {
// Stack this decorator (topmost-first) onto any decorators the inner
// FnDef already carries from decorators written below this one.
let this_dec = { "name": dec_name, "args": dec_args }
let existing = inner["decorators"]
let dlist = native_list_empty()
let dlist = native_list_append(dlist, this_dec)
let ne: Int = native_list_len(existing)
let ei = 0
while ei < ne {
let dlist = native_list_append(dlist, native_list_get(existing, ei))
let ei = ei + 1
}
let with_dec = {
"stmt": "FnDef",
"name": inner["name"],
"params": inner["params"],
"body": inner["body"],
"ret_type": inner["ret_type"],
"decorator": dec_name
"decorator": dec_name,
"decorators": dlist
}
// r result map fully consumed release to free peak heap.
el_release(r)
+8 -1
View File
@@ -305,7 +305,14 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
// prefix and add it if present (no-op on Linux where libssl is in /usr/lib).
let ossl_lib_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-L{}/lib' 2>/dev/null || true)"
let ossl_inc_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)"
let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
// Force-include the C-level master declarations header so every translation
// unit sees all cross-module function signatures. Handles packages (like ELP)
// where modules call each other without explicit El import statements.
// The header is generated by elb --gen-decls or manually placed in out_dir.
let master_decls: String = out_dir + "/elp-c-decls.h"
let has_master: String = str_trim(exec_capture("test -f " + master_decls + " && echo yes || echo no"))
let include_flag: String = if str_eq(has_master, "yes") { "-include " + master_decls } else { "" }
let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
let i = 0
while i < n {
let f: String = native_list_get(c_files, i)
File diff suppressed because it is too large Load Diff
@@ -117,6 +117,15 @@ el_val_t el_min(el_val_t a, el_val_t b);
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── Arena scoping ────────────────────────────────────────────────────────────
* el_arena_push() activates the string arena (if not already active) and
* returns a mark; el_arena_pop(mark) frees all strings allocated since that
* mark. Used by codegen for per-function/statement scoping and by long-running
* EL loops (e.g. the soul daemon's awareness tick) to reclaim per-iteration
* allocations. */
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
@@ -142,6 +151,7 @@ el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
el_val_t http_delete_json(el_val_t url, el_val_t json_body);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
@@ -167,6 +177,11 @@ void http_set_handler(el_val_t name);
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Non-blocking variant of http_serve: runs the accept loop in a background
* pthread and returns immediately so the caller can continue (used by the
* soul daemon to run awareness_run() after starting its HTTP API). */
void http_serve_async(el_val_t port, el_val_t handler);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
* returned string carries the discriminator `{"el_http_response":1,...}`
@@ -576,6 +591,7 @@ el_val_t engram_list_layers(void);
el_val_t engram_get_node(el_val_t id);
void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
el_val_t engram_node_count(void);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
@@ -594,13 +610,40 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_act_stats_json(void);
el_val_t engram_text_health_json(void);
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
/* Destructively pop up to `max` newly-formed Hebbian associations as a JSON
* array of {from_id,to_id,weight,hebb}. The learning process (soul daemon) is
* not the process that owns persistence (engram HTTP server); this is how a
* self-formed association crosses that boundary. (2026-08-07 self-review.) */
el_val_t engram_hebb_drain_json(el_val_t max);
/* Document frequency of a term across node labels — term-specificity signal
* for curiosity seed selection. (2026-08-03 self-review.) */
el_val_t engram_label_df(el_val_t term);
/* Best curiosity seed from one node: argmax over idf·position·casing across
* the candidate tokens of its label, falling back to its content when the
* label is a sentinel. Excludes pipe-delimited tabu terms during selection
* and gates candidates to the df band [min_df, max_df]. Returns "" when
* nothing qualifies. (2026-08-13 self-review.) */
el_val_t engram_salient_term(el_val_t node_id, el_val_t max_df,
el_val_t min_df, el_val_t tabu);
el_val_t engram_embed_backfill(el_val_t count);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
el_val_t engram_wm_count(void);
el_val_t engram_wm_avg_weight(void);
el_val_t engram_wm_top_json(el_val_t n);
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
el_val_t engram_load_merge(el_val_t path);
/* engram_compile_layered_json — produce a prompt-ready text block split
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
File diff suppressed because it is too large Load Diff
+883
View File
@@ -0,0 +1,883 @@
/*
* el_runtime.h El language C runtime header
*
* Declares all built-in functions available to compiled El programs.
* Include this in every generated .c file.
*
* Value model:
* All El values are represented as el_val_t (= int64_t).
* On 64-bit systems a pointer fits in int64_t.
* String values are cast: (el_val_t)(uintptr_t)"hello"
* Integer values are stored directly.
* This lets arithmetic work naturally while still passing strings around.
*
* Type conventions (El -> C):
* String -> el_val_t (holds const char* via uintptr_t cast)
* Int -> el_val_t
* Bool -> el_val_t (0 = false, nonzero = true)
* Any -> el_val_t
* Void -> void
*
* Macros for convenience:
* EL_STR(s) cast string literal to el_val_t
* EL_CSTR(v) cast el_val_t back to const char*
* EL_INT(v) identity el_val_t is already int64_t
*
* Link requirements:
* -lcurl required for the HTTP client (http_get, http_post, llm_*).
* -lpthread required for the HTTP server (one detached thread per
* connection, capped at 64 concurrent).
* -loqs optional; required only when liboqs is installed and the
* pq_* / sha3_256_hex entry points are needed. Detected at
* compile time via __has_include(<oqs/oqs.h>).
* -lcrypto optional; pulled in alongside -loqs. Used for X25519 in
* pq_hybrid_* and HKDF-SHA256 derivation.
*
* Canonical compile command:
* cc -std=c11 -I runtime -lcurl -lpthread \
* -o <out> <prog>.c runtime/el_runtime.c
*
* With liboqs (post-quantum stack):
* cc -std=c11 -I runtime -lcurl -lpthread -loqs -lcrypto \
* -o <out> <prog>.c runtime/el_runtime.c
*/
#pragma once
#include <stdint.h>
#include <stdlib.h>
typedef int64_t el_val_t;
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
#define EL_INT(v) (v)
#define EL_NULL ((el_val_t)0)
/* Float values share the el_val_t (int64) slot via a bit-cast.
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
* underlying bits represent the IEEE 754 double. Float-aware builtins
* (math, format, json) round-trip via these helpers. */
static inline double el_to_float(el_val_t v) {
union { int64_t i; double f; } u;
u.i = (int64_t)v;
return u.f;
}
static inline el_val_t el_from_float(double f) {
union { double f; int64_t i; } u;
u.f = f;
return (el_val_t)u.i;
}
#ifdef __cplusplus
extern "C" {
#endif
/* ── I/O ──────────────────────────────────────────────────────────────────── */
void println(el_val_t s);
void print(el_val_t s);
el_val_t readline(void);
/* ── String builtins ─────────────────────────────────────────────────────── */
el_val_t el_str_concat(el_val_t a, el_val_t b);
el_val_t str_eq(el_val_t a, el_val_t b);
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
el_val_t str_len(el_val_t s);
el_val_t str_concat(el_val_t a, el_val_t b);
el_val_t int_to_str(el_val_t n);
el_val_t str_to_int(el_val_t s);
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
el_val_t str_contains(el_val_t s, el_val_t sub);
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
el_val_t str_to_upper(el_val_t s);
el_val_t str_to_lower(el_val_t s);
el_val_t str_trim(el_val_t s);
/* ── Math ────────────────────────────────────────────────────────────────── */
el_val_t el_abs(el_val_t n);
el_val_t el_max(el_val_t a, el_val_t b);
el_val_t el_min(el_val_t a, el_val_t b);
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
* Lists and Maps carry a refcount. Strings and ints do not el_retain and
* el_release are safe no-ops on non-refcounted values (they sniff a magic
* header at offset 0 and only act if the magic matches).
*
* Codegen emits these at let-binding shadowing, function entry (params), and
* function exit (locals other than the returned value). The refcount lets
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
* and copy-on-write when shared (preserves persistent semantics across
* accumulator patterns in the compiler itself). */
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── Arena scoping ────────────────────────────────────────────────────────────
* el_arena_push() activates the string arena (if not already active) and
* returns a mark; el_arena_pop(mark) frees all strings allocated since that
* mark. Used by codegen for per-function/statement scoping and by long-running
* EL loops (e.g. the soul daemon's awareness tick) to reclaim per-iteration
* allocations. */
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
el_val_t el_list_len(el_val_t list);
el_val_t el_list_get(el_val_t list, el_val_t index);
el_val_t el_list_append(el_val_t list, el_val_t elem);
el_val_t el_list_empty(void);
el_val_t el_list_clone(el_val_t list);
/* ── Map ─────────────────────────────────────────────────────────────────── */
el_val_t el_map_new(el_val_t pair_count, ...);
el_val_t el_get_field(el_val_t map, el_val_t key);
el_val_t el_map_get(el_val_t map, el_val_t key);
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
el_val_t http_get(el_val_t url);
el_val_t http_post(el_val_t url, el_val_t body);
el_val_t http_post_json(el_val_t url, el_val_t json_body);
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
el_val_t http_delete_json(el_val_t url, el_val_t json_body);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
/* HTTP server v2 ─────────────────────────────────────────────────────────────
* Same dispatch model as http_serve, but the handler signature is widened:
*
* el_val_t handler(method, path, headers_map, body)
*
* `headers_map` is an ElMap from lowercased header name header value (both
* Strings). Repeated headers are joined with ", " per RFC 7230.
*
* Response value: the handler may return either
* (a) a plain body string same auto-content-type / 200-OK behaviour as
* http_serve (3-arg) or
* (b) a response envelope built with `http_response(status, headers_json,
* body)`. The runtime detects the envelope discriminator
* `"el_http_response":1` at the start of the returned string and
* unpacks status / headers / body before sending.
*
* The 3-arg http_serve(port, handler) remains supported unchanged for
* existing handlers (e.g. products/web/server.el): it dispatches with
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Non-blocking variant of http_serve: runs the accept loop in a background
* pthread and returns immediately so the caller can continue (used by the
* soul daemon to run awareness_run() after starting its HTTP API). */
void http_serve_async(el_val_t port, el_val_t handler);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
* returned string carries the discriminator `{"el_http_response":1,...}`
* which the runtime's send-path detects and unpacks. Detection happens
* uniformly inside http_send_response, so a 3-arg handler may also return
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
* auto-content-type contract for legacy handlers that return plain bodies. */
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
* 60000ms). Read lazily on first use, so setting the env var any time before
* the first http_* call is sufficient. */
/* Streaming variants — write the response body straight to a file via
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
* embedded NUL bytes that would truncate a strlen()-based code path.
*
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
* `headers_map` shape as http_post_with_headers (ElMap of StringString).
*
* Return value: 1 on success (file fully written), 0 on any failure
* (network, file open, partial write). On failure the output file is removed
* so callers cannot mistake a partially-written file for a valid one. */
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
/* ── URL encoding ────────────────────────────────────────────────────────── */
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
* el_html_sanitize(input_html, allowlist_json) strict allowlist HTML
* cleaner. State-machine parser; tag/attribute names compared case-
* insensitively against the allowlist; `<a href>` / `< src>` URL schemes
* validated (http, https, mailto, fragment-only, or relative); whole-
* subtree drop for script / style / iframe / object / embed / form; HTML-
* escapes free text outside dropped subtrees.
*
* The allowlist is JSON of the form
* {"p":[],"a":["href","title"],"strong":[],...}
* where each value is the array of attribute names allowed for that tag. */
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
el_val_t fs_write(el_val_t path, el_val_t content);
el_val_t fs_list(el_val_t path);
el_val_t fs_exists(el_val_t path);
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
* byte count). The caller knows the length from context typically because
* `bytes` came from base64_decode (which produces a magic-tagged binary
* buffer with embedded NULs possible) and the caller already tracks the
* decoded length, OR because the bytes came from a fixed-size source
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
*
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
* write, negative length). On partial-write failure, the file is removed
* so callers cannot read back a truncated artefact. */
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
/* ── JSON ────────────────────────────────────────────────────────────────── */
el_val_t json_get(el_val_t json, el_val_t key);
el_val_t json_parse(el_val_t s);
el_val_t json_stringify(el_val_t v);
el_val_t json_get_string(el_val_t json_str, el_val_t key);
el_val_t json_get_int(el_val_t json_str, el_val_t key);
el_val_t json_get_float(el_val_t json_str, el_val_t key);
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
el_val_t json_array_len(el_val_t json_str);
el_val_t json_array_get(el_val_t json_str, el_val_t index);
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
/* ── Time ────────────────────────────────────────────────────────────────── */
el_val_t time_now(void);
el_val_t time_now_utc(void);
el_val_t sleep_secs(el_val_t secs);
el_val_t sleep_ms(el_val_t ms);
el_val_t time_format(el_val_t ts, el_val_t fmt);
el_val_t time_to_parts(el_val_t ts);
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
/* ── Instant + Duration: first-class temporal types ──────────────────────────
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
* is enforced at codegen-time: BinOps on names registered as Instant or
* Duration route through the typed wrappers below; mismatches like
* Instant+Instant become #error at the C compiler.
*
* Postfix literals `30.seconds`, `1.hour`, `500.millis`, `30.nanos` are
* recognised by the parser as DurationLit AST nodes and lowered to literal
* int64 nanoseconds at codegen time. The runtime never sees the units. */
el_val_t el_now_instant(void);
el_val_t now(void);
el_val_t unix_seconds(el_val_t n);
el_val_t unix_millis(el_val_t n);
el_val_t instant_from_iso8601(el_val_t s);
el_val_t el_duration_from_nanos(el_val_t ns);
el_val_t duration_seconds(el_val_t n);
el_val_t duration_millis(el_val_t n);
el_val_t duration_nanos(el_val_t n);
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
el_val_t el_instant_diff(el_val_t a, el_val_t b);
el_val_t el_duration_add(el_val_t a, el_val_t b);
el_val_t el_duration_sub(el_val_t a, el_val_t b);
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
el_val_t el_instant_lt(el_val_t a, el_val_t b);
el_val_t el_instant_le(el_val_t a, el_val_t b);
el_val_t el_instant_gt(el_val_t a, el_val_t b);
el_val_t el_instant_ge(el_val_t a, el_val_t b);
el_val_t el_instant_eq(el_val_t a, el_val_t b);
el_val_t el_instant_ne(el_val_t a, el_val_t b);
el_val_t el_duration_lt(el_val_t a, el_val_t b);
el_val_t el_duration_le(el_val_t a, el_val_t b);
el_val_t el_duration_gt(el_val_t a, el_val_t b);
el_val_t el_duration_ge(el_val_t a, el_val_t b);
el_val_t el_duration_eq(el_val_t a, el_val_t b);
el_val_t el_duration_ne(el_val_t a, el_val_t b);
el_val_t instant_to_unix_seconds(el_val_t i);
el_val_t instant_to_unix_millis(el_val_t i);
el_val_t instant_to_iso8601(el_val_t i);
el_val_t duration_to_seconds(el_val_t d);
el_val_t duration_to_millis(el_val_t d);
el_val_t duration_to_nanos(el_val_t d);
el_val_t el_sleep_duration(el_val_t dur);
el_val_t unix_timestamp(void);
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
el_val_t ttl_cache_age(el_val_t key);
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
* domains.
*
* A Calendar interprets an Instant under a particular cycle convention and
* produces a CalendarTime. CalendarTime carries the underlying Instant and
* a back-pointer to its Calendar; arithmetic and formatting consult the
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
* (or sol/phase, or cycle/phase, depending on kind).
*
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
* LocalDateTime are heap-allocated structs whose pointers are cast into
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
* the kind safely. LocalTime is small enough to live in the int64 slot
* directly (nanos since midnight, signed). */
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
* on first use of the owning EarthCalendar. */
el_val_t zone(el_val_t id);
el_val_t zone_utc(void);
el_val_t zone_local(void);
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
* allocated, magic-tagged Calendar struct. Calendars are interned by
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
* the same pointer equality is reference equality. */
el_val_t earth_calendar(el_val_t z);
el_val_t earth_calendar_default(void);
el_val_t mars_calendar(void);
el_val_t cycle_calendar(el_val_t period_dur);
el_val_t no_cycle_calendar(void);
el_val_t relative_calendar(el_val_t epoch_inst);
/* CalendarTime constructors and methods. Returns a heap-allocated struct
* whose pointer fits in el_val_t. */
el_val_t now_in(el_val_t cal);
el_val_t in_calendar(el_val_t inst, el_val_t cal);
el_val_t cal_format(el_val_t ct, el_val_t pattern);
el_val_t cal_to_instant(el_val_t ct);
el_val_t cal_cycle_phase(el_val_t ct);
el_val_t cal_in(el_val_t ct, el_val_t cal);
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
* LocalTime carries nanoseconds since midnight as a signed int64 directly
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
* heap-allocated structs with magic headers. */
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
el_val_t local_datetime(el_val_t date, el_val_t time);
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
el_val_t local_date_year(el_val_t ld);
el_val_t local_date_month(el_val_t ld);
el_val_t local_date_day(el_val_t ld);
el_val_t local_time_hour(el_val_t lt);
el_val_t local_time_minute(el_val_t lt);
el_val_t local_time_second(el_val_t lt);
el_val_t local_time_nanos(el_val_t lt);
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
* pointer in el_val_t; rhythms are immutable so callers may share them. */
el_val_t rhythm_cycle_start(void);
el_val_t rhythm_cycle_phase(el_val_t phase);
el_val_t rhythm_duration(el_val_t d);
el_val_t rhythm_session_start(void);
el_val_t rhythm_event(el_val_t name);
el_val_t rhythm_and(el_val_t a, el_val_t b);
el_val_t rhythm_or(el_val_t a, el_val_t b);
el_val_t rhythm_weekday(el_val_t day);
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
/* ── UUID ────────────────────────────────────────────────────────────────── */
el_val_t uuid_new(void);
el_val_t uuid_v4(void);
/* ── Environment ─────────────────────────────────────────────────────────── */
el_val_t env(el_val_t key);
/* ── In-process state K/V ────────────────────────────────────────────────── */
el_val_t state_set(el_val_t key, el_val_t value);
el_val_t state_get(el_val_t key);
el_val_t state_del(el_val_t key);
el_val_t state_keys(void);
/* ── Float formatting ────────────────────────────────────────────────────── */
el_val_t float_to_str(el_val_t f);
el_val_t int_to_float(el_val_t n);
el_val_t float_to_int(el_val_t f);
el_val_t format_float(el_val_t f, el_val_t decimals);
el_val_t decimal_round(el_val_t f, el_val_t decimals);
el_val_t str_to_float(el_val_t s);
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
el_val_t math_sqrt(el_val_t f);
el_val_t math_log(el_val_t f);
el_val_t math_ln(el_val_t f);
el_val_t math_sin(el_val_t f);
el_val_t math_cos(el_val_t f);
el_val_t math_pi(void);
/* ── String additions ────────────────────────────────────────────────────── */
el_val_t str_index_of(el_val_t s, el_val_t sub);
el_val_t str_split(el_val_t s, el_val_t sep);
el_val_t str_char_at(el_val_t s, el_val_t i);
el_val_t str_char_code(el_val_t s, el_val_t i);
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
el_val_t str_format(el_val_t fmt, el_val_t data);
el_val_t str_lower(el_val_t s);
el_val_t str_upper(el_val_t s);
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
* is_* predicates: empty input returns false; multi-char requires ALL bytes
* to match. ASCII ranges only in Phase 1. */
/* Counting */
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
el_val_t str_count_chars(el_val_t s); /* codepoint count */
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
el_val_t str_count_lines(el_val_t s);
el_val_t str_count_words(el_val_t s);
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
/* Find / position */
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
/* Transform */
el_val_t str_repeat(el_val_t s, el_val_t n);
el_val_t str_reverse(el_val_t s); /* by codepoint */
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
el_val_t str_lstrip(el_val_t s);
el_val_t str_rstrip(el_val_t s);
/* Char classification (Bool) */
el_val_t is_letter(el_val_t s);
el_val_t is_digit(el_val_t s);
el_val_t is_alphanumeric(el_val_t s);
el_val_t is_whitespace(el_val_t s);
el_val_t is_punctuation(el_val_t s);
el_val_t is_uppercase(el_val_t s);
el_val_t is_lowercase(el_val_t s);
/* Split / join */
el_val_t str_split_lines(el_val_t s);
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
/* ── List additions ──────────────────────────────────────────────────────── */
el_val_t list_push(el_val_t list, el_val_t elem);
el_val_t list_push_front(el_val_t list, el_val_t elem);
el_val_t list_join(el_val_t list, el_val_t sep);
el_val_t list_range(el_val_t start, el_val_t end);
/* ── Bool helpers ────────────────────────────────────────────────────────── */
el_val_t bool_to_str(el_val_t b);
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
el_val_t parse_int(el_val_t s, el_val_t default_val);
/* ── Process ─────────────────────────────────────────────────────────────── */
void exit_program(el_val_t code);
el_val_t getpid_now(void);
/* ── CGI identity ─────────────────────────────────────────────────────────────
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
* Records the program's DHARMA identity before any other code executes. */
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
el_val_t network, el_val_t engram);
/* ── DHARMA network builtins ─────────────────────────────────────────────────
* Available to CGI programs (declared with a `cgi {}` block).
*
* Peers are addressed by `dharma_id` of the form
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
* If the @<url> portion is omitted, transport defaults to
* "http://localhost:7770" (the local CGI daemon assumption).
*
* Wire protocol (all peers expose):
* POST <url>/dharma/recv { channel, from, content } response body
* POST <url>/dharma/event { type, payload, source, timestamp }
* POST <url>/api/activate { query } list of nodes
*
* Hosting application's responsibility: an El program with a `cgi {}` block
* runs http_serve() with its own request handler; that handler should route
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
* incoming events feed dharma_field() queues. The runtime itself does not
* intercept any /dharma path. */
el_val_t dharma_connect(el_val_t cgi_id);
el_val_t dharma_send(el_val_t channel, el_val_t content);
el_val_t dharma_activate(el_val_t query);
void dharma_emit(el_val_t event_type, el_val_t payload);
el_val_t dharma_field(el_val_t event_type);
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
el_val_t dharma_relationship(el_val_t cgi_id);
el_val_t dharma_peers(void);
/* Public C API: called by an El program's HTTP handler when a /dharma/event
* request arrives. Pushes onto the per-event-type queue and signals any
* pending dharma_field() blockers. All three arguments must be NUL-terminated
* C strings (or NULL then treated as empty). */
void el_runtime_dharma_event_arrive(const char* event_type,
const char* payload,
const char* source);
/* ── Engram local graph primitives ───────────────────────────────────────────
* Operate on the CGI's local Engram knowledge graph.
* `engram_activate` queries the local graph only; `dharma_activate` is
* network-wide across all connected CGI graphs. */
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
el_val_t salience, el_val_t importance, el_val_t confidence,
el_val_t tier, el_val_t tags);
/* Layered consciousness — see el_runtime.c for the layered architecture
* design notes (search "Layered consciousness architecture"). The five
* canonical layers (safety / core-identity / domain-knowledge / imprint /
* suit) are seeded automatically; engram_add_layer extends the registry
* with imprint or suit overlays at runtime. Nodes default to layer 1
* (core-identity) when created via engram_node / engram_node_full. */
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
el_val_t salience, el_val_t certainty, el_val_t confidence,
el_val_t status, el_val_t tags, el_val_t layer_id);
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
el_val_t transparent, el_val_t injectable);
el_val_t engram_remove_layer(el_val_t layer_id);
el_val_t engram_list_layers(void);
el_val_t engram_get_node(el_val_t id);
void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
el_val_t engram_node_count(void);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
el_val_t engram_neighbors(el_val_t node_id);
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_edge_count(void);
/* Three-pass activation: background fan-out → working-memory promotion →
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
el_val_t engram_activate(el_val_t query, el_val_t depth);
el_val_t engram_save(el_val_t path);
el_val_t engram_load(el_val_t path);
/* Tiered paged-store entry points (ENGRAM_STORE=1). engram_store_boot opens the
* durable store (import-once / WAL-replay) and loads it resident; checkpoint pushes
* the resident graph's current field state (incl. learned hebb + activation-formed
* edges) through the WAL and flushes; close checkpoints + closes. No-ops when off. */
el_val_t engram_store_boot(el_val_t data_dir);
el_val_t engram_store_checkpoint(void);
el_val_t engram_store_close(void);
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_retrieve_geometric_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_emb_json(el_val_t limit, el_val_t offset);
el_val_t engram_dreams_json(el_val_t since_ms);
/* §5 geometry operators as EL builtins (read-only; seed-id CSV args). */
el_val_t engram_geo_descriptor_json(el_val_t seeds);
el_val_t engram_geo_overlap_json(el_val_t a_seeds, el_val_t b_seeds);
el_val_t engram_geo_subtract_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t mode);
el_val_t engram_geo_combine_json(el_val_t a_seeds, el_val_t b_seeds);
el_val_t engram_geo_distance_json(el_val_t a_seeds, el_val_t b_seeds);
el_val_t engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds);
/* reasoning layer (compositions over §5 operators). ANALOGY maps cleanly to the
* flat-CSV seed ABI; the other modes take set/point/timestamp inputs deferred from
* this ABI (see engram_reason.h / the reasoning-operators runbook). */
el_val_t engram_reason_analogy_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t c_seeds);
/* COGNITION (2026-08-14): THE ONE OPERATION + grounding, surfaced live. */
el_val_t engram_think_json(el_val_t seeds, el_val_t faculty);
el_val_t engram_ground_json(el_val_t claim, el_val_t evidence, el_val_t for_whom);
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor);
el_val_t engram_attend_json(el_val_t node_id, el_val_t observer, el_val_t salience);
el_val_t engram_correspondence_beat_json(el_val_t seeds, el_val_t faculty, el_val_t keystone);
el_val_t engram_consolidate_permanence(el_val_t node_id);
el_val_t engram_age_field(el_val_t delta_ms);
el_val_t engram_age_field_catchup(void);
el_val_t engram_chrono_persist_tick(void);
el_val_t engram_chrono_tick(void);
el_val_t engram_boundary_beat(el_val_t op_name); /* API-reshape decorator-seam auto-emit */
el_val_t engram_self_anchor_capture(void);
el_val_t engram_self_drift_json(void);
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_act_stats_json(void);
el_val_t engram_text_health_json(void);
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
/* M10 reified-neighborhood read-only HTTP surface (2026-08-13). List/detail of
* the resident reify index; [] until the offline reify writer has run. */
el_val_t engram_geo_reify_list_json(void);
el_val_t engram_geo_reify_get_json(el_val_t id);
/* WRITE: run reification — persist Neighborhood nodes + member edges, rebuild the
* resident index. Wires the previously-dormant engram_geo_reify_store. */
el_val_t engram_geo_reify_run_json(void);
/* SELF-REIFICATION (2026-08-14). ON-BEAT autonomous neighborhood formation:
* gated by ENGRAM_SELF_REIFY (default off returns {"enabled":false}, writes
* nothing the live binary is byte-inert until the flag is set). When on, runs
* ONE bounded, incremental, idempotent reification pass (change-detection skips
* unchanged hubs no re-append; grounded names; residue-preserving supersession;
* soft/overlapping membership; nests only when something changed). Meant to be
* pumped every heartbeat next to Hebbian consolidation. Returns
* {"enabled":true,"reified":N,"skipped":S,"superseded":P,"nested":X,"resident":M,"wrote":b}. */
el_val_t engram_self_reify_beat_json(void);
/* ASYNC EXPLICIT OVERRIDE (degenerate manual case). Rename a live neighborhood
* by id: writes a superseding record with the new name, prepends a residue entry
* (cause="explicit-override", prior name), keeps the same geometry/members. Never
* blocks the beat. Returns {"ok":true,"renamed":<old>,"new_id":<new>,"name":..}. */
el_val_t engram_neighborhood_rename_json(el_val_t id, el_val_t name);
/* Orphan prevention: form up to k semantic-similar edges to a node's nearest
* embedded neighbors (kNN). engram_nearest_json is the read-only probe. */
el_val_t engram_autoconnect_node(el_val_t id, el_val_t k, el_val_t min_sim_pct);
el_val_t engram_nearest_json(el_val_t id, el_val_t k);
/* Telemetry off-graph: append one ISE JSON line to the state-event log tier. */
el_val_t engram_ise_log_append(el_val_t content);
/* Destructively pop up to `max` newly-formed Hebbian associations as a JSON
* array of {from_id,to_id,weight,hebb}. The learning process (soul daemon) is
* not the process that owns persistence (engram HTTP server); this is how a
* self-formed association crosses that boundary. (2026-08-07 self-review.) */
el_val_t engram_hebb_drain_json(el_val_t max);
/* Document frequency of a term across node labels — term-specificity signal
* for curiosity seed selection. (2026-08-03 self-review.) */
el_val_t engram_label_df(el_val_t term);
/* Best curiosity seed from one node: argmax over idf·position·casing across
* the candidate tokens of its label, falling back to its content when the
* label is a sentinel. Excludes pipe-delimited tabu terms during selection
* and gates candidates to the df band [min_df, max_df]. Returns "" when
* nothing qualifies. (2026-08-13 self-review.) */
el_val_t engram_salient_term(el_val_t node_id, el_val_t max_df,
el_val_t min_df, el_val_t tabu);
el_val_t engram_embed_backfill(el_val_t count);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from runtime on 2026-06-30 self-review. */
el_val_t engram_wm_count(void);
el_val_t engram_wm_avg_weight(void);
el_val_t engram_wm_top_json(el_val_t n);
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
el_val_t engram_load_merge(el_val_t path);
/* ── WAL + compaction + integrity (ENGRAM_WAL=on; design doc §§3-14,§18) ──── */
int engram_wal_enabled(void);
el_val_t engram_crc32(el_val_t s);
el_val_t engram_wal_boot(el_val_t dir); /* replay + open; returns records */
el_val_t engram_wal_open_dir(el_val_t dir);
el_val_t engram_wal_node_put(el_val_t dir, el_val_t id);
el_val_t engram_wal_edges_since(el_val_t dir, el_val_t start_count);
el_val_t engram_wal_hebb_batch(el_val_t dir, el_val_t start_count);
el_val_t engram_wal_forget(el_val_t dir, el_val_t id);
el_val_t engram_wal_compact(el_val_t dir);
el_val_t engram_wal_maybe_compact(el_val_t dir);
el_val_t engram_resolve_data_dir(void); /* §18.2 fail-loud default */
el_val_t engram_is_protected(el_val_t id); /* §18.1/18.3 derived set */
el_val_t engram_protected_json(void);
/* engram_compile_layered_json — produce a prompt-ready text block split
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
* no nodes promoted to working memory. */
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
* All functions call https://api.anthropic.com/v1/messages with the API key
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
el_val_t llm_call(el_val_t model, el_val_t prompt);
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
/* LLM token telemetry (CCR §4.4): usage.{input,output}_tokens parsed from every
* response. Returns Map{input_tokens,output_tokens,total_input_tokens,
* total_output_tokens,calls}. */
el_val_t llm_last_usage(void);
/* Fold usage.{input,output}_tokens from a raw messages response into the counters.
* Called internally on every LLM response; exported for offline testing. */
void llm_record_usage(const char* resp);
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
el_val_t llm_models(void);
/* Register a tool handler by name. The handler is looked up via dlsym
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
* a global C symbol that this function can locate at runtime.
* Handler signature: `el_val_t handler(el_val_t input_json)` receives
* the tool input as a JSON-string el_val_t and returns a JSON-string
* el_val_t result. Used by llm_call_agentic. */
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
/* ── args() ─────────────────────────────────────────────────────────────────
* Provides access to command-line arguments passed to the program.
* Populated by el_runtime_init_args() before main() runs. */
el_val_t args(void);
void el_runtime_init_args(int argc, char** argv);
/* ── Crypto primitives ─────────────────────────────────────────────────────
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
* Self-contained no OpenSSL/libcrypto dependency. The implementations are
* adapted from public-domain reference code (Brad Conte / RFC 4648).
*
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
* value whose contents are raw binary; callers usually feed these into
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
* so the binary payload may contain embedded NULs pass it directly into
* base64_encode (which uses an explicit length) rather than treating it as
* a printable C string.
*
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
* as used in JWTs. */
el_val_t sha256_hex(el_val_t input);
el_val_t sha256_bytes(el_val_t input);
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
el_val_t base64_encode(el_val_t input);
el_val_t base64_decode(el_val_t input);
el_val_t base64url_encode(el_val_t input);
el_val_t base64url_decode(el_val_t input);
/* Length-aware variants (internal — exposed for the rare caller that already
* has a known-length binary buffer and doesn't want to round-trip through
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
* these implicitly. */
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
* All inputs/outputs hex-encoded. Algorithm choices:
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
*
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
* time), the pq_* entry points return a JSON-shaped error string so callers
* fail loudly rather than silently fall back to classical schemes:
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
*
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
* Keccak permutation is PQ-OK as a primitive). */
el_val_t pq_keygen_signature(void);
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
el_val_t pq_kem_keygen(void);
el_val_t pq_kem_encaps(el_val_t public_key_hex);
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
el_val_t pq_hybrid_keygen(void);
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
el_val_t sha3_256_hex(el_val_t input);
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
* Symmetric authenticated encryption used to wrap envelopes after a KEM
* handshake. Caller MUST supply a 32-byte key (64 hex chars) typically the
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
*
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
* Nonce is a fresh 12-byte CSPRNG draw callers never pick the nonce, which
* structurally rules out the GCM nonce-reuse footgun.
*
* aead_decrypt returns the plaintext String, or "" on any failure (including
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
* These match the El VM's native_* builtins so that El source compiled
* to C can call the same names without modification. */
el_val_t native_list_get(el_val_t list, el_val_t index);
el_val_t native_list_len(el_val_t list);
el_val_t native_list_append(el_val_t list, el_val_t elem);
el_val_t native_list_empty(void);
el_val_t native_list_clone(el_val_t list);
el_val_t native_string_chars(el_val_t s);
el_val_t native_int_to_str(el_val_t n);
/* ── Method-call shorthand aliases ──────────────────────────────────────────
* The El method-call convention `obj.method(args)` compiles to
* `method(obj, args)`. These aliases expose the runtime functions under
* the short names that result from method calls in El source.
*
* Example: `myList.append(x)` `append(myList, x)` (calls this alias)
* `myList.len()` `len(myList)` (calls this alias) */
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
el_val_t len(el_val_t list); /* el_list_len */
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
/* See bottom of el_runtime.c for the implementation.
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
/* ── Subprocess execution ────────────────────────────────────────────────── */
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
el_val_t trace_span_start(el_val_t name);
el_val_t trace_span_end(el_val_t span_handle);
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
#ifdef __cplusplus
}
#endif
+43 -3
View File
@@ -6,15 +6,55 @@
//
// Dependencies: runtime/string.el, runtime/json.el
// --- Validation (defense in depth) ---
// el_val_t is an untyped machine word, so a wrong TYPE can't be caught here but a
// wrong VALUE can (a tier in the node_type slot, an empty/garbage string, an int, a
// path, a model name, a cgi id). Reject loudly instead of silently writing junk.
fn engram_valid_node_type(t: String) -> Bool {
return str_eq(t, "Memory") || str_eq(t, "Knowledge") || str_eq(t, "Belief")
|| str_eq(t, "Project") || str_eq(t, "Tag") || str_eq(t, "BacklogItem")
|| str_eq(t, "Artifact") || str_eq(t, "Conversation") || str_eq(t, "ExecutionContext")
|| str_eq(t, "InternalStateEvent") || str_eq(t, "Self") || str_eq(t, "Entity")
|| str_eq(t, "Process") || str_eq(t, "ConfigEntry") || str_eq(t, "Concept") || str_eq(t, "Imprint")
|| str_eq(t, "SessionSummary")
}
fn engram_valid_tier(t: String) -> Bool {
return str_eq(t, "Semantic") || str_eq(t, "Episodic") || str_eq(t, "Working")
|| str_eq(t, "Procedural") || str_eq(t, "Canonical") || str_eq(t, "Note") || str_eq(t, "Lesson")
}
// --- Node creation ---
fn engram_node(content: String, node_type: String, salience: Float) -> String {
if !engram_valid_node_type(node_type) {
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "'")
return ""
}
return __engram_node(content, node_type, salience)
}
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
source: String, lang: String, ts: Int, tags: String) -> String {
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
// Signature MUST match the C primitive __engram_node_full exactly (el_seed.h):
// (content, node_type, label, salience, importance, confidence, tier, tags)
// The previous wrapper declared a stale 8-arg schema with wrong names AND types
// (sal:Float at the label slot, ts:Int at the tier slot). Because el_val_t is an
// untyped machine word, the EL compiler coerced caller args to those wrong param
// types and then forwarded them BY POSITION into the C function so tier received
// an int, importance/confidence received strings, label received a float, etc.
// That is the field-corruption bug. Match the contract 1:1 no coercion, no reorder.
fn engram_node_full(content: String, node_type: String, label: String,
salience: Float, importance: Float, confidence: Float,
tier: String, tags: String) -> String {
if !engram_valid_node_type(node_type) {
__println("[engram] REJECTED node write — invalid node_type '" + node_type + "' (label=" + label + ")")
return ""
}
if !engram_valid_tier(tier) {
__println("[engram] REJECTED node write — invalid tier '" + tier + "' (node_type=" + node_type + ", label=" + label + ")")
return ""
}
return __engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags)
}
// --- Node retrieval ---
+339
View File
@@ -0,0 +1,339 @@
/* engram_cognition.c — THE ONE OPERATION. See engram_cognition.h.
* Pure over its inputs (think/warp/express); persistence is additive/supersede
* only. stdlib + libm + engram_store/reason/geometry. Touches no live daemon. */
#include "engram_cognition.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
/* ── small helpers ──────────────────────────────────────────────────────────── */
static double vdot(const float* a, const float* b, int dim) {
double s = 0; for (int i = 0; i < dim; i++) s += (double)a[i] * (double)b[i]; return s;
}
static double vnorm(const float* a, int dim) { return sqrt(vdot(a, a, dim)); }
static char* dupstr(const char* s) {
if (!s) return NULL; size_t n = strlen(s) + 1; char* p = malloc(n);
if (p) memcpy(p, s, n); return p;
}
static double clampd(double x, double lo, double hi){ return x<lo?lo:(x>hi?hi:x); }
/* ═══════════════════════════════════════════════ Stance lifecycle ════════════ */
int cog_stance_init(CogStance* s, const char* id, const char* faculty,
const char* anchor_region, const char* for_whom,
const GeoDescriptor* region) {
if (!s || !region) return -1;
memset(s, 0, sizeof *s);
s->id = dupstr(id); s->faculty = dupstr(faculty);
s->anchor_region = dupstr(anchor_region); s->for_whom = dupstr(for_whom);
s->dim = region->dim;
s->n_axes = region->n_axes > COG_MAX_AXES ? COG_MAX_AXES : region->n_axes;
for (int k = 0; k < COG_MAX_AXES; k++) s->axis_gain[k] = 1.0;
s->ext_floor = 1.0; s->drop_frac = 0.5; s->assoc_floor = 0.2;
s->bias_dir = NULL;
s->reliability = 0.5; /* uninformed prior on our own track record */
return 0;
}
void cog_stance_set_frozen_defaults(CogStance* s) {
if (!s) return;
for (int k = 0; k < COG_MAX_AXES; k++) s->axis_gain[k] = 1.0;
s->ext_floor = 1.0; s->drop_frac = 0.5; s->assoc_floor = 0.2;
free(s->bias_dir); s->bias_dir = NULL;
}
void cog_stance_free(CogStance* s) {
if (!s) return;
free(s->id); free(s->faculty); free(s->anchor_region); free(s->for_whom);
free(s->bias_dir);
s->id = s->faculty = s->anchor_region = s->for_whom = NULL; s->bias_dir = NULL;
}
int cog_is_keystone(const CogKeystoneSet* ks, const CogStance* s) {
if (!s) return 0;
if (s->keystone) return 1;
if (!ks || !s->id) return 0;
for (int i = 0; i < ks->n; i++)
if (ks->ids[i] && (strcmp(ks->ids[i], s->id) == 0 ||
(s->anchor_region && strcmp(ks->ids[i], s->anchor_region) == 0))) return 1;
return 0;
}
/* ═══════════════════════════════════════════════ warped fit (think step 2) ══ */
int cog_warped_fit(const GeoDescriptor* g, const float* x,
const CogStance* st, GeoFit* out) {
if (!g || !x || !out || g->dim <= 0 || !g->centroid) return -1;
double ext_floor = (st && st->ext_floor > 0) ? st->ext_floor : 1.0;
int dim = g->dim;
double rr = 0;
float* r = malloc((size_t)dim * sizeof(float));
if (!r) return -1;
for (int i = 0; i < dim; i++) { double d = (double)x[i] - (double)g->centroid[i]; r[i] = (float)d; rr += d * d; }
double maha2 = 0, ss_in = 0;
for (int k = 0; k < g->n_axes; k++) {
const float* ax = g->axes[k].axis; if (!ax) continue;
double proj = vdot(r, ax, dim);
double gain = (st && k < st->n_axes && st->axis_gain[k] > 0) ? st->axis_gain[k] : 1.0;
double den = g->axes[k].extent * gain; if (den < ext_floor) den = ext_floor;
maha2 += (proj / den) * (proj / den);
ss_in += proj * proj;
}
double ortho2 = rr - ss_in; if (ortho2 < 0) ortho2 = 0;
double dist2 = maha2 + ortho2 / (ext_floor * ext_floor);
out->mahalanobis = sqrt(maha2); out->ortho_residual = sqrt(ortho2);
out->distance = sqrt(dist2); out->score = 1.0 / (1.0 + dist2);
free(r);
return 0;
}
/* ═══════════════════════════════════════════════ think (the ONE operation) ══ */
void engram_gradient_free(GeoGradient* g) {
if (!g) return; free(g->direction); g->direction = NULL;
}
int engram_think(const GeoDescriptor* region, const float* anchor,
const CogStance* stance, GeoGradient* out) {
if (!region || !out || region->dim <= 0 || !region->centroid) return -1;
int dim = region->dim;
memset(out, 0, sizeof *out);
out->dim = dim;
const float* x = anchor ? anchor : region->centroid; /* re-origin (step 1) */
GeoFit f;
if (cog_warped_fit(region, x, stance, &f) != 0) return -1; /* fit (step 2) */
/* step 3 — emit a GRADIENT: warped steepest DESCENT of the fit distance². */
double ext_floor = (stance && stance->ext_floor > 0) ? stance->ext_floor : 1.0;
float* grad = calloc((size_t)dim, sizeof(float)); /* ∇ dist² wrt x */
float* r = malloc((size_t)dim * sizeof(float));
out->direction = malloc((size_t)dim * sizeof(float));
if (!grad || !r || !out->direction) { free(grad); free(r); free(out->direction); out->direction = NULL; return -1; }
for (int i = 0; i < dim; i++) r[i] = (float)((double)x[i] - (double)region->centroid[i]);
/* in-subspace: Σ_k 2 (proj/den²) a_k ; also accumulate Σ proj a_k for ortho part */
float* proj_sum = calloc((size_t)dim, sizeof(float));
if (!proj_sum) { free(grad); free(r); free(out->direction); out->direction = NULL; return -1; }
for (int k = 0; k < region->n_axes; k++) {
const float* ax = region->axes[k].axis; if (!ax) continue;
double proj = vdot(r, ax, dim);
double gain = (stance && k < stance->n_axes && stance->axis_gain[k] > 0) ? stance->axis_gain[k] : 1.0;
double den = region->axes[k].extent * gain; if (den < ext_floor) den = ext_floor;
double coef = 2.0 * proj / (den * den);
for (int i = 0; i < dim; i++) { grad[i] += (float)(coef * ax[i]); proj_sum[i] += (float)(proj * ax[i]); }
}
/* orthogonal: (2 r 2 Σ proj a_k) / ext_floor² */
double inv_f2 = 1.0 / (ext_floor * ext_floor);
for (int i = 0; i < dim; i++)
grad[i] += (float)((2.0 * (double)r[i] - 2.0 * (double)proj_sum[i]) * inv_f2);
free(proj_sum);
/* steering = grad (descent), seeded by the stance's bias_dir. */
for (int i = 0; i < dim; i++) out->direction[i] = -grad[i];
if (stance && stance->bias_dir) {
double gn = vnorm(grad, dim), bn = vnorm(stance->bias_dir, dim);
if (bn > 1e-12) {
double scale = (gn > 1e-12 ? gn : 1.0); /* seed at the gradient's scale */
for (int i = 0; i < dim; i++)
out->direction[i] += (float)(scale * (double)stance->bias_dir[i] / bn);
}
}
double dn = vnorm(out->direction, dim);
if (dn > 1e-12) for (int i = 0; i < dim; i++) out->direction[i] /= (float)dn;
else for (int i = 0; i < dim; i++) out->direction[i] = 0.0f; /* at rest */
out->spread = f.distance; /* spiked (0) .. diffuse */
out->confidence = stance ? stance->reliability : 0.5;
out->magnitude = f.score; /* the read's membership */
out->anchor_id = region->hub_id; /* borrowed vantage id */
out->n_support = region->n_members;
out->stance_id = stance ? stance->id : NULL;
free(grad); free(r);
return 0;
}
/* EXPRESSION — the ONLY collapse to a point (a separate faculty from think). */
int engram_express(const GeoGradient* g, const float* anchor, float* out_point) {
if (!g || !anchor || !out_point || !g->direction) return -1;
double commit = clampd(g->confidence, 0.0, 1.0); /* confident => commit far */
for (int i = 0; i < g->dim; i++)
out_point[i] = anchor[i] + g->direction[i] * (float)commit;
return 0;
}
/* ═══════════════════════════════════════════════ Stance serialization ════════ */
/* Compact line schema "STNC1" (mirrors the reify "GEO1" precedent). */
char* cog_stance_to_metadata(const CogStance* s) {
if (!s) return NULL;
size_t cap = 256 + (size_t)s->n_axes * 24 + (size_t)(s->bias_dir ? s->dim * 16 : 0);
char* buf = malloc(cap); if (!buf) return NULL;
size_t o = 0;
o += (size_t)snprintf(buf + o, cap - o, "%s\n", COG_STANCE_META_MAGIC);
o += (size_t)snprintf(buf + o, cap - o, "f %s\n", s->faculty ? s->faculty : "-");
o += (size_t)snprintf(buf + o, cap - o, "r %s\n", s->anchor_region ? s->anchor_region : "-");
o += (size_t)snprintf(buf + o, cap - o, "w %s\n", s->for_whom ? s->for_whom : "-");
o += (size_t)snprintf(buf + o, cap - o, "k %d\n", s->keystone);
o += (size_t)snprintf(buf + o, cap - o, "d %d %d\n", s->dim, s->n_axes);
o += (size_t)snprintf(buf + o, cap - o, "s %.9g %.9g %.9g\n", s->ext_floor, s->drop_frac, s->assoc_floor);
o += (size_t)snprintf(buf + o, cap - o, "g");
for (int k = 0; k < s->n_axes; k++) o += (size_t)snprintf(buf + o, cap - o, " %.9g", s->axis_gain[k]);
o += (size_t)snprintf(buf + o, cap - o, "\n");
o += (size_t)snprintf(buf + o, cap - o, "c %lld %.9g %.9g %.9g %.9g\n",
(long long)s->n_trials, s->brier_sum, s->reliability, s->ema_error, s->last_error);
if (s->bias_dir) {
o += (size_t)snprintf(buf + o, cap - o, "b");
for (int i = 0; i < s->dim; i++) o += (size_t)snprintf(buf + o, cap - o, " %.9g", (double)s->bias_dir[i]);
o += (size_t)snprintf(buf + o, cap - o, "\n");
}
(void)o;
return buf;
}
int cog_stance_to_node(const CogStance* s, StoreNode* out) {
if (!s || !out) return -1;
memset(out, 0, sizeof *out);
out->id = dupstr(s->id);
out->node_type = dupstr(COG_STANCE_NODE_TYPE);
out->content = dupstr(s->faculty ? s->faculty : "stance");
out->label = dupstr(s->faculty ? s->faculty : "stance");
out->metadata = cog_stance_to_metadata(s);
out->importance = s->reliability; /* cached denormalized readout (§2.1) */
out->confidence = s->reliability;
out->temporal_decay_rate = 0.0;
return (out->id && out->node_type && out->metadata) ? 0 : -1;
}
static int parse_floats(const char* line, double* out, int max) {
int n = 0; const char* p = line;
while (*p && n < max) {
while (*p == ' ') p++;
if (!*p) break;
char* end; double v = strtod(p, &end);
if (end == p) break;
out[n++] = v; p = end;
}
return n;
}
int cog_stance_from_node(const StoreNode* n, CogStance* out) {
if (!n || !out || !n->metadata) return -1;
memset(out, 0, sizeof *out);
for (int k = 0; k < COG_MAX_AXES; k++) out->axis_gain[k] = 1.0;
out->ext_floor = 1.0; out->drop_frac = 0.5; out->assoc_floor = 0.2; out->reliability = 0.5;
out->id = dupstr(n->id);
/* verify magic on first line */
const char* m = n->metadata;
if (strncmp(m, COG_STANCE_META_MAGIC, strlen(COG_STANCE_META_MAGIC)) != 0) return -1;
char* copy = dupstr(m); if (!copy) return -1;
for (char* line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
if (line[0] == '\0' || line[1] != ' ') {
if (line[0] == 'g' || line[0] == 'b') { /* vector lines: tag then values */ }
else continue;
}
char tag = line[0];
const char* rest = line + 1; while (*rest == ' ') rest++;
if (tag == 'f') { free(out->faculty); out->faculty = (strcmp(rest, "-") ? dupstr(rest) : NULL); }
else if (tag == 'r') { free(out->anchor_region); out->anchor_region = (strcmp(rest, "-") ? dupstr(rest) : NULL); }
else if (tag == 'w') { free(out->for_whom); out->for_whom = (strcmp(rest, "-") ? dupstr(rest) : NULL); }
else if (tag == 'k') { out->keystone = atoi(rest); }
else if (tag == 'd') { int a=0,b=0; sscanf(rest, "%d %d", &a, &b); out->dim = a; out->n_axes = b > COG_MAX_AXES ? COG_MAX_AXES : b; }
else if (tag == 's') { double v[3]={1,0.5,0.2}; parse_floats(rest, v, 3); out->ext_floor=v[0]; out->drop_frac=v[1]; out->assoc_floor=v[2]; }
else if (tag == 'g') { double v[COG_MAX_AXES]; int c=parse_floats(rest, v, COG_MAX_AXES); for(int k=0;k<c;k++) out->axis_gain[k]=v[k]; }
else if (tag == 'c') { double v[5]={0,0,0.5,0,0}; parse_floats(rest, v, 5); out->n_trials=(int64_t)v[0]; out->brier_sum=v[1]; out->reliability=v[2]; out->ema_error=v[3]; out->last_error=v[4]; }
else if (tag == 'b') { if (out->dim>0){ out->bias_dir=calloc((size_t)out->dim,sizeof(float)); double v[4096]; int c=parse_floats(rest,v,out->dim<4096?out->dim:4096); for(int i=0;i<c;i++) out->bias_dir[i]=(float)v[i]; } }
}
free(copy);
return 0;
}
/* ═══════════════════════════════════════════════ grounding as a RELATION ═════ */
static int put_edge(EngramPagedStore* s, const char* id, const char* from, const char* to,
const char* relation, double weight, const char* meta) {
StoreEdge e; memset(&e, 0, sizeof e);
e.id = (char*)id; e.from_id = (char*)from; e.to_id = (char*)to;
e.relation = (char*)relation; e.weight = weight; e.confidence = weight;
e.metadata = (char*)meta;
return store_put_edge(s, &e);
}
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
const char* evidence_id, double grounding, const char* for_whom) {
if (!s || !claim_id || !evidence_id) return -1;
char id[512], meta[256];
snprintf(id, sizeof id, "gb-%s-%s-%s", claim_id, evidence_id, for_whom ? for_whom : "global");
snprintf(meta, sizeof meta, "for_whom=%s", for_whom ? for_whom : "-");
return put_edge(s, id, claim_id, evidence_id, COG_GROUNDED_BY_RELATION, grounding, meta);
}
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
const char* observer_id, double salience) {
if (!s || !node_id || !observer_id) return -1;
char id[512];
snprintf(id, sizeof id, "st-%s-%s", node_id, observer_id);
return put_edge(s, id, node_id, observer_id, COG_SALIENT_TO_RELATION, salience, NULL);
}
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
const char* for_whom, double floor) {
if (!s || !claim_id) return -1;
if (!(floor > 0)) floor = 0.5;
StoreEdge* edges = NULL; size_t n = 0;
if (store_get_edges_from(s, claim_id, &edges, &n) < 0) return -1;
double best = 0.0; int found = 0;
for (size_t i = 0; i < n; i++) {
if (!edges[i].relation || strcmp(edges[i].relation, COG_GROUNDED_BY_RELATION) != 0) continue;
/* grounded-for-whom: match observer if requested; global (for_whom=-) always counts */
int match = 1;
if (for_whom && edges[i].metadata) {
const char* fw = strstr(edges[i].metadata, "for_whom=");
if (fw) { fw += 9; if (strcmp(fw, for_whom) != 0 && strcmp(fw, "-") != 0) match = 0; }
}
if (match) { found = 1; if (edges[i].weight > best) best = edges[i].weight; }
}
store_edges_free(edges, n);
if (!found) return 0; /* ungrounded => refuse assertion (still held) */
return (best >= floor) ? 1 : 0;
}
/* ═══════════════════════════════════════════════ THE CORRESPONDENCE-LOOP ═════ */
int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
double outcome_y, CogStance* stance,
int learn, double max_step, CogBeatResult* out) {
if (!region || !stance || !out) return -1;
memset(out, 0, sizeof *out);
if (stance->keystone) { learn = 0; out->wrote_keystone = 1; } /* §6: never write a keystone */
GeoGradient g;
if (engram_think(region, anchor, stance, &g) != 0) return -1; /* PREDICTION */
double p = g.magnitude;
double y = clampd(outcome_y, 0.0, 1.0);
double err = fabs(p - y);
out->correspondence = 1.0 - err;
out->error = err;
out->brier = (p - y) * (p - y);
if (learn) {
/* refine warp: gradient descent of (py)² wrt each axis_gain.
* p = 1/(1+D²); p/gain_k = 2 p² proj_k² / (ext_k² gain_k³) (>=0)
* (err²)/gain_k = 2 (py) p/gain_k
* step = lr · (err²)/gain_k, bounded to ±max_step (metastability). */
int dim = region->dim;
const float* x = anchor ? anchor : region->centroid;
float* r = malloc((size_t)dim * sizeof(float));
if (r) {
for (int i = 0; i < dim; i++) r[i] = (float)((double)x[i] - (double)region->centroid[i]);
double lr = 0.5;
double bound = (max_step > 0) ? max_step : 0.05; /* bounded update rate */
for (int k = 0; k < region->n_axes && k < stance->n_axes; k++) {
const float* ax = region->axes[k].axis; if (!ax) continue;
double proj = vdot(r, ax, dim);
double ext = region->axes[k].extent; if (ext < 1e-9) ext = 1e-9;
double gain = stance->axis_gain[k]; if (gain < 1e-6) gain = 1e-6;
double dp_dgain = 2.0 * p * p * (proj * proj) / (ext * ext * gain * gain * gain);
double dErr_dgain = 2.0 * (p - y) * dp_dgain;
double step = -lr * dErr_dgain;
step = clampd(step, -bound, bound);
stance->axis_gain[k] = clampd(gain + step, 0.1, 50.0);
}
free(r);
}
/* calibration */
stance->n_trials += 1;
stance->brier_sum += out->brier;
stance->last_error = err;
stance->ema_error = (stance->n_trials == 1) ? err : 0.9 * stance->ema_error + 0.1 * err;
double mean_brier = stance->brier_sum / (double)stance->n_trials;
stance->reliability = clampd(1.0 - sqrt(mean_brier), 0.0, 1.0);
}
out->reliability = stance->reliability;
engram_gradient_free(&g);
return 0;
}
+216
View File
@@ -0,0 +1,216 @@
/* engram_cognition.h — THE ONE OPERATION.
*
* The buildable form of the "cognition is one operation" theory (design doc
* engram/spec/cognitive-architecture.design.md; memory bdc8a488 / d582a766).
*
* Cognition is ONE operation think a directed traversal-READ of the geometry
* from an anchor, steered by a learned STANCE, whose output is a GRADIENT (a
* direction + spread over the geometry), never a point. The named faculties
* (reason / induce / abduce / analogy / relate / plan / ground) are human LABELS
* on regions of think's steering space: each faculty == { think + a named stance }.
* Collapse-to-a-point happens only at EXPRESSION (a separate faculty), never in think.
*
* NAMING (Will's directive): the surface verbs name the cognitive ACT being
* performed (think / reason / induce / ground / verify), not the internal function
* shape. The single frozen primitive underneath every faculty is engram_think,
* which composes over engram_reason_point_fit + the §5 geo-algebra. Those never
* learn. Only the STANCE learns.
*
* "Stance" is the theory's steering PRIOR, deliberately named distinctly: in this
* codebase the token "prior" already means previous-VERSION (supersession). A
* Stance is a learnable bias/disposition over the geometry which axes matter,
* which way pays off, plus a calibrated track record attached to a faculty-label
* and a region, and grounded-for-whom.
*
* PURE + (mostly) READ-ONLY, stdlib + libm only. think() and the warp are pure
* over their inputs. Persistence (Stance <-> StoreNode, grounded-by edges) is the
* only part that touches the store, and it is additive / supersede / tombstone
* never mutate-in-place, never delete. It NEVER touches the live daemon: all
* offline against a scratch store, per the design's rails.
*/
#ifndef ENGRAM_COGNITION_H
#define ENGRAM_COGNITION_H
#include <stdint.h>
#include <stddef.h>
#include "engram_geometry.h"
#include "engram_reason.h"
#include "engram_store.h"
/* Max principal axes a stance warps (matches GeoParams.top_axes default budget). */
#define COG_MAX_AXES 32
/* ═══════════════════════════════════════════════════════════════════════════
* §1 GeoGradient the OUTPUT of think. A direction + spread over the geometry,
* plus the calibrated confidence and the read it was computed against. NOT a point.
* A spiked gradient (spread0) = "exact" (deduction); a spread gradient = "fuzzy"
* (prediction). The gradient is ALSO the next steering direction (closed-loop flow).
* */
typedef struct {
int dim;
float* direction; /* unit steering vector in the anchor's frame (owned) */
double spread; /* 0 = spiked/exact ... large = diffuse/fuzzy */
double confidence; /* calibrated, from the stance's track record (reliab.) */
double magnitude; /* THIS read's own membership/fit estimate ∈(0,1].
* The scalar an expression faculty would SAMPLE; kept
* on the gradient but never used AS a decision by think.*/
const char* anchor_id; /* borrowed: the vantage this was read from */
int n_support; /* neighborhood members that shaped the read */
const char* stance_id; /* borrowed: which stance steered this (provenance) */
} GeoGradient;
void engram_gradient_free(GeoGradient* g);
/* ═══════════════════════════════════════════════════════════════════════════
* §2 Stance the learnable steering prior, as a first-class object. In memory
* here; persisted as a StoreNode (node_type "Stance") via cog_stance_*serialize.
*
* warp: axis_gain[] per-principal-axis multiplier on extents (which axes
* matter gain>1 WIDENS an axis so it penalizes less);
* bias_dir[] a steering-direction seed in the region's frame;
* scalars faculty constants this stance overrides (ext_floor, etc).
* calibration: the track record the ONLY thing the loop (§4) updates
* besides warp: n_trials, a Brier accumulator, reliability
* ( GeoGradient.confidence), and an EMA error.
* keystone: if set, the correspondence-loop MUST NEVER write warp or
* calibration read-mostly (self / values). §6 metastability.
* */
typedef struct {
char* id; /* stance node id (owned) */
char* faculty; /* the act this stance serves: "induce"|"relate"|... */
char* anchor_region; /* node/neighborhood id this stance is attached to */
char* for_whom; /* observer id — grounding is relational (NULL=global) */
int keystone; /* 1 = read-mostly, loop never writes it (§6) */
int dim; /* embedding dim of the region */
int n_axes; /* how many axis_gain entries are live (<= COG_MAX_AXES)*/
double axis_gain[COG_MAX_AXES]; /* per-axis extent multipliers (init 1.0) */
float* bias_dir; /* dim floats, steering seed (owned; NULL = none) */
double ext_floor; /* faculty scalar: the extent floor (init 1.0) */
double drop_frac; /* faculty scalar (causal): confound drop (init 0.5) */
double assoc_floor; /* faculty scalar (causal): assoc floor (init 0.2) */
/* calibration / track record */
int64_t n_trials;
double brier_sum; /* Σ (p y)² */
double reliability; /* calibrated ∈[0,1] → GeoGradient.confidence */
double ema_error; /* EMA of per-trial error */
double last_error;
} CogStance;
/* Initialize a neutral stance (all gains 1.0, default scalars, reliability 0.5).
* dim/n_axes taken from the region descriptor. faculty/id/for_whom are copied. */
int cog_stance_init(CogStance* s, const char* id, const char* faculty,
const char* anchor_region, const char* for_whom,
const GeoDescriptor* region);
void cog_stance_free(CogStance* s);
/* A stance set to today's hard-coded constants == behavioral parity with the
* pre-stance operators (axis_gain all 1.0, ext_floor default, drop_frac 0.5,
* assoc_floor 0.2). This is the FROZEN CONTROL used by the validation. */
void cog_stance_set_frozen_defaults(CogStance* s);
/* ── Serialization: Stance <-> StoreNode (compact line schema "STNC1", mirroring
* the reify "GEO1" precedent). Additive; the node's importance field caches the
* reliability readout. Round-trips exactly (reboot-prove). */
char* cog_stance_to_metadata(const CogStance* s); /* owned string */
int cog_stance_to_node(const CogStance* s, StoreNode* out);/* fills a StoreNode */
int cog_stance_from_node(const StoreNode* n, CogStance* out);/* parse STNC1 */
#define COG_STANCE_NODE_TYPE "Stance"
#define COG_STANCE_META_MAGIC "STNC1"
/* ═══════════════════════════════════════════════════════════════════════════
* §1.2 think the ONE operation. Frozen procedure over three steps:
* 1. re-origin on the anchor point (the vantage; the manifold is the read
* neighborhood, passed as `region`);
* 2. fit the anchor under the stance's WARP (engram_reason_point_fit with the
* axis extents multiplied by axis_gain and ext_floor substituted);
* 3. emit a GRADIENT: direction = the warped steepest-descent that reduces the
* fit distance (the "which way pays off" seed + bias_dir), spread from the
* fit distance, confidence from the stance's reliability, magnitude = the
* read's membership estimate. NO point-collapse that is expression.
*
* `region` the read neighborhood (built by vantage_read / geometry descriptor).
* `anchor` the point to read FROM (dim floats). NULL = region centroid (self).
* `stance` the steering prior. NULL = neutral (frozen defaults) => parity.
* Returns 0 and fills `out` (engram_gradient_free), <0 on error.
* */
int engram_think(const GeoDescriptor* region, const float* anchor,
const CogStance* stance, GeoGradient* out);
/* The warped fit itself (step 2), exposed for the loop + verifier reuse. Identical
* to engram_reason_point_fit when stance==NULL or all gains==1 && ext_floor default. */
int cog_warped_fit(const GeoDescriptor* region, const float* x,
const CogStance* stance, GeoFit* out);
/* EXPRESSION — the ONLY place a gradient collapses to a point. Samples the gradient
* off the anchor along its steering direction, scaled by (1 spread) so a spiked
* (confident) gradient lands a definite point and a diffuse one barely moves.
* This is deliberately a SEPARATE faculty from think (§1.2, M5). */
int engram_express(const GeoGradient* g, const float* anchor, float* out_point);
/* ═══════════════════════════════════════════════════════════════════════════
* §5 HOLD vs GROUND vs ASSERT. Holding is unconditional (the store gates nothing).
* Grounding is a RELATION a "grounded-by" edge, probabilistic, grounded-for-whom.
* The honesty floor is checked only at ASSERTION.
* */
#define COG_GROUNDED_BY_RELATION "grounded-by"
#define COG_SALIENT_TO_RELATION "salient-to"
/* Write a grounded-by edge (additive). weight = grounding ∈(0,1] from the verifier;
* for_whom recorded in edge metadata (grounding is relational). Never a node flag. */
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
const char* evidence_id, double grounding, const char* for_whom);
/* Write/refresh a salient-to edge: salience is RELATIONAL (grounded-for-whom),
* carried on the edge to the observer not baked into the node scalar (§2.1). */
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
const char* observer_id, double salience);
/* The honesty floor — a QUERY at assertion time, NOT a schema constraint. Reads the
* claim's stored grounded-by edges (for the given observer) and returns:
* 1 = may assert (best grounding >= floor),
* 0 = REFUSE assertion (holds unconditionally; only asserting is gated),
* <0 = error. The content remains held either way. */
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
const char* for_whom, double floor);
/* ═══════════════════════════════════════════════════════════════════════════
* §4 THE REFLEXIVE CORRESPONDENCE-LOOP the learning engine. think scores its
* OWN gradient against outcome, refines the stance on the error, and (optionally)
* writes the (gradient, outcome, error) back as self-describing geometry. This is
* the dormant verifier turned INWARD.
*
* grade(1) SELF-CONSISTENCY (no external world-labels): the outcome is what the
* geometry itself says the membership determined by the region's SIGNAL subspace
* (the axes reality actually weights). The stance's cheap warped read is graded
* against that geometric truth; error refines the warp so the read corresponds.
* */
typedef struct {
double correspondence; /* ∈[0,1]: 1 |p y| for this trial */
double error; /* 1 correspondence */
double brier; /* running mean (p y)² across the stance's trials */
double reliability; /* the stance's current calibrated reliability */
int wrote_keystone; /* 1 iff a keystone update was BLOCKED (safety audit) */
} CogBeatResult;
/* One correspondence beat for ONE trial:
* think(region, anchor, stance) -> gradient (a PREDICTION, ungrounded)
* outcome y := grade-1 self-consistency target (in [0,1])
* error = |magnitude y|; refine stance.warp + calibration on the error
* (bounded step; NEVER writes a keystone stance)
* `learn`==0 grades WITHOUT updating (the frozen-control path). `max_step` bounds
* the per-beat warp change (metastability; §6). Returns 0 / <0. */
int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
double outcome_y, CogStance* stance,
int learn, double max_step, CogBeatResult* out);
/* ═══════════════════════════════════════════════════════════════════════════
* §6 METASTABILITY. Keystones (self/values) are read-mostly: the loop reads but
* never writes them. Mark by stance flag or by a keystone-id set the loop consults.
* */
typedef struct { const char** ids; int n; } CogKeystoneSet;
int cog_is_keystone(const CogKeystoneSet* ks, const CogStance* s);
#endif /* ENGRAM_COGNITION_H */
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More