Compare commits

..

21 Commits

Author SHA1 Message Date
will.anderson e08150d6e7 elb: fix monolithic link, capability violations, liboqs detection
- Link only the entry-point .c (monolithic) instead of all module .c
  files — prevents duplicate symbol errors from inlined imports
- Strip capability-violation #error guards post-compile; they fire
  incorrectly when modules are compiled in isolation but linked into
  a CGI program
- Add liboqs (post-quantum) include/lib detection via brew, matching
  the existing OpenSSL detection pattern
2026-06-05 11:34:28 -05:00
will.anderson e3a72aae90 self-review 2026-06-05: WM composition visibility + ISE tier-migration fix
Two improvements from daily self-review:

1. engram_wm_top_json(n) builtin — returns top-N WM nodes by weight as
   compact JSON [{label,node_type,tier,wm},...]. After long uptime all WM
   nodes cycle in steady-state decay+re-promotion so wm_promotion ISEs
   never fire (only trigger on 0→>0.1 transitions). This gives continuous
   visibility into WM composition on every heartbeat.

2. ISE tier-migration exclusion — InternalStateEvent nodes no longer
   participate in tier migration. ISEs are activated by curiosity_scan
   substring matches and accumulated 50+ activation_count, reaching
   Procedural tier (720h decay). This permanently crowded WM with ephemeral
   state events, hiding the Knowledge/Memory nodes that should dominate.
   Fix: skip ISEs in the TIER MIGRATION pass; they stay on Working-tier
   48h decay regardless of activation frequency.

wm_top_json is called from awareness.el emit_heartbeat (top-5 snapshot).
2026-06-05 11:34:28 -05:00
will.anderson 1264af72a6 self-review 2026-06-04: lower WM threshold, soften inhibition, add wm_avg_weight builtin
Three targeted improvements based on graph health analysis (29K nodes, 104 edges):

1. ENGRAM_WM_THRESHOLD 0.15 → 0.08: sparse graph means BFS paths carry weak
   signals (0.05-0.12 range). Prior threshold gatekept too aggressively. Grounded
   in TBRS* cognitive model (θ=0.05); 0.08 is conservative but effective.

2. ENGRAM_INHIBITION_FACTOR 0.1 → 0.2: factor=0.1 (90% suppression) on a sparse
   graph almost always fully silences targeted nodes. Factor=0.2 (80% suppression)
   maintains strong inhibition while allowing partially-suppressed nodes to remain
   faintly active — consistent with partial inhibition in cognitive neuroscience.

3. engram_wm_avg_weight() builtin: computes mean working_memory_weight of all
   promoted nodes. Returns float bits via el_from_float for EL float_to_str usage.
   Makes activation quality directly observable in heartbeat ISEs, distinguishing
   'many weak activations' (sparse, low avg) from 'few strong' (dense, high avg).

Rebuilt engram binary with new runtime.
2026-06-05 11:34:28 -05:00
will.anderson 412bd2744e self-review 2026-05-29: fix dampening floor and cleanup route_create_node auto-link
Two changes:

1. el_runtime.c — engram_activation_dampen(): add floor of 0.35.
   ISE nodes with ac=900+ had dampen=0.128, giving effective salience=0.038
   which fell below the epist>=0.1 gate in engram_activate. This silently
   killed curiosity seeds "self identity values" and "decision pattern lesson"
   — the only corpus matches were high-ac ISEs that were then excluded from
   results, causing activated=0 on 50% of proactive_curiosity scans.
   Floor at 0.35 keeps salience=0.3 nodes at effective_bg=0.105, above the
   visibility threshold, without disrupting relative ordering of content nodes.

2. server.el — route_create_node: replace stale inline auto-link with
   auto_link_content_node(). The inline logic used the old engram_search_json
   (substring, no ISE filter) while the better BM25-based auto_link_content_node
   was added in 2026-05-28 and wired to /api/neuron/* routes but not to the
   raw /api/nodes POST path. Removes ~40 lines of duplicated logic.
2026-06-05 11:34:28 -05:00
will.anderson a000599bfe self-review 2026-05-28: checkpoint ISE decay fix + auto-linking for MCP nodes
Three changes:

1. Fix checkpoint ISE temporal_decay_rate: engram_emit_ise_internal was
   hardcoded to 0.0 (global 168h default) instead of 2.310 (Working-tier
   48h). Result: checkpoint ISEs accumulated at 3.5x intended rate.

2. Raise CHECKPOINT_INTERVAL 1→10: checkpoint ISE fires on every single
   node write, producing 2:1 checkpoint:content ratio in ISE stream.
   MCP routes still call engram_write_binary_el explicitly after each
   important write, so no knowledge durability is lost.

3. Add auto_link_content_node to server.el: route_neuron_memory and
   route_neuron_knowledge_capture were creating nodes with zero edges —
   invisible to BFS traversal, only reachable via lexical/semantic seed.
   New helper runs BM25 over top-20 results, skips ISE nodes (which
   dominate the 14K-node corpus), connects up to 3 related nodes.
2026-06-05 11:34:27 -05:00
will.anderson 8f922e68b3 self-review 2026-05-27: semantic auto-linking on embed
Add engram_auto_link_semantic(): when a node is embedded, scan all
embedded nodes for cosine sim >= 0.72 and create bidirectional
"semantic-similar" edges to the top-3 matches. Runs once at write
time rather than at every query, converting the expensive O(N) scan
from live activation into durable graph structure.

Fixes the core connectivity problem: 63 edges / 5364 nodes (0.012
edges/node). Verified: new Knowledge nodes now auto-link at sim 0.77–
0.95 with correct threshold discrimination. One checkpoint per insert
(not one per edge) bounds the persistence overhead.

Excludes InternalStateEvents and inbox/outbox transient nodes to keep
semantic graph clean.
2026-06-05 11:34:27 -05:00
will.anderson ef1db34846 self-review 2026-05-24: BM25 scan cap 500→5000 + traversal inference guard
Two improvements:

1. BM25 search corpus coverage (server.el) — raised scan cap from 500 to
   5000 nodes. On the 161K-node graph, 500 was 0.3% coverage; 5000 is 3%.
   engram_scan_nodes_json returns nodes sorted by salience DESC, so ISEs
   (salience 0.3) fall below Knowledge/Memory (0.5–0.8) naturally — the
   effective corpus stays content-dense. Also updated stale comment on the
   ISE route (no longer need high offset; recent-first ordering from May 23).

2. Traversal inference guard (el_runtime.c) — two changes:
   - INFER_CAP reduced 256→32: proactive curiosity runs engram_activate
     every ~30s. At 256 edges/call the soul daemon accumulated 107K edges
     in 23h (5× BFS slowdown). At 32 the rate drops 8×.
   - Edge count guard: skip inference entirely when snap_ec ≥ 40,000.
     At that density most A→C paths are already explicit; marginal inference
     value is low and the O(edge_count²) inner-loop cost is high. Self-limits
     unbounded accumulation across restarts.
2026-06-05 11:34:27 -05:00
will.anderson 34249b39a3 self-review 2026-05-23: ISE recent-first ordering + http_serve_async builtin
Two improvements:

1. ISE scan ordering — engram_scan_nodes_by_type_json now sorts InternalStateEvent
   nodes by created_at DESC (most-recent-first) instead of salience DESC. Old
   high-salience ISEs (session-start, wm-promotion) no longer dominate offset 0,
   burying recent heartbeat and curiosity_scan events at offsets 20000+. New
   behavior: ?limit=10 returns the 10 most recent ISEs regardless of salience.
   All other node types retain existing salience-sorted behavior.

2. http_serve_async registered as elc builtin — added to builtin_arity() in both
   codegen.el (EL compiler source) and dist/platform/elc.c (compiled C). Also
   rebuilt elc binary from updated elc.c. This closes the fragile-patch gap from
   2026-05-21: elc previously treated http_serve_async as an unknown identifier,
   and the gap description noted elc would 'silently revert to blocking http_serve'
   on next soul rebuild. Now http_serve_async has a proper 2-arg arity entry and
   will survive all future soul recompiles without a manual neuron.c patch.
2026-06-05 11:34:27 -05:00
will.anderson 7b45468b1c self-review 2026-05-20: relation-type boost in BFS propagation
Add relation-specific multipliers to spreading activation:
- causes/caused_by edges: 2.0× (causal reasoning propagates stronger)
- enables/prevents edges: 1.5× (conditional logic gets boost)
- supersedes edges: 1.3× (promoted knowledge gets slight priority)
- inferred A→C edges: 0.7× (traversal-inferred paths weaker than explicit)

Field-validated against Hindsight (time-aware spreading activation, 2026)
and ACT-R cognitive architecture literature. Inferred edge discount prevents
the traversal inference pass from flooding activation with spurious high-
strength paths equal to explicit links.
2026-06-05 11:34:27 -05:00
will.anderson db7dae8236 self-review 2026-05-19: ACT-R WM persistence — decay non-reached nodes instead of zeroing
The activation persist step was writing wm_weight=0 for every node not reached
by the current BFS fan-out. This destroyed working memory accumulated by
MCP-layer activations within one tick of the awareness loop firing on an empty
inbox. ACT-R and Soar treat spreading activation as additive: absent seeds
contribute zero spread, not a zero override of existing WM state.

Fix: non-reached nodes now decay by ENGRAM_WM_DECAY (0.7) per activation call
rather than being immediately zeroed. A hard floor of 0.005 clears near-zero
values to prevent infinite decay tails. Reached nodes behave unchanged.
2026-06-05 11:34:27 -05:00
will.anderson ee1627c2c0 self-review 2026-05-18: raise SEM_FLOOR to 0.70, add ACT-R frequency resistance to decay
Semantic seed floor raised 0.65 → 0.70: field literature (SuperLocalMemory
arXiv:2604.04514) validates 0.70-0.75 as the noise floor for sentence-transformer
embeddings on non-trivial corpora. The 0.65 threshold was admitting false
positives that diluted BFS traversal quality on the 13K-node post-ingestor graph.
Top-30 cap retained — sparse graph (1.26 edges/node) needs more semantic entry
points than a dense graph would.

ACT-R Base-Level Learning insight applied to engram_temporal_decay: current
purely time-based formula treated a node activated 100 times identically to a
node never activated (same decay rate). ACT-R BLL (Anderson 1993) shows
frequently-accessed memories resist temporal decay. Fix: freq_resist multiplier
= 1.0 + 0.1 * log(1 + activation_count). count=0→1.0×, count=9→1.23×,
count=99→1.46×. Existing activation_dampening continues to reduce per-query
novelty for well-known nodes — the two mechanisms are complementary: resist
decay (durable in graph), dampen per-query (don't dominate any single turn).
2026-06-05 11:34:27 -05:00
will.anderson b90333e9e7 self-review 2026-05-17: semantic seeding in Pass 1 + WM promotion observability
Three improvements from daily review:

1. Add semantic seed supplement to Pass 1 activation (el_runtime.c).
   Previously, engram_cosine_sim was only called in goal_bias (Pass 2) for
   nodes that already matched lexically. Nodes semantically close but
   lexically disjoint were completely invisible to activation. With 8K+
   world-ingestor nodes added overnight, this was a critical gap. Now: after
   lexical seeding, scan un-seeded nodes for cosine sim ≥ 0.65 and inject
   top-30 as additional seeds. Sem seeds get 85% of full act to stay weaker
   than exact lexical matches.

2. Lower WM promotion ISE threshold from >0.5 to >0.1 (el_runtime.c).
   Only one wm-promotion ISE was ever logged — the 0.5 floor was too high.
   Most practical Knowledge/Memory promotions are in the 0.1-0.5 range.
   Lowering to 0.1 makes working memory activity visible in state events.
2026-06-05 11:34:27 -05:00
will.anderson d917165aaf self-review 2026-05-16: tier-based decay rates, implement knowledge_promote, ISE label extraction
Three research-grounded improvements:

1. Tier-based temporal decay in el_runtime.c (engram_node_full, engram_node_layered):
   Working=48h, Episodic=72h, Semantic=336h, Procedural=720h half-lives.
   Grounded in ACT-R literature — differentiated decay by chunk type. The
   temporal_decay_rate field existed but was always 0 (global 168h for everything).
   New nodes now carry the correct half-life for their tier from creation.

2. Implement route_neuron_knowledge_promote in server.el (was a silent stub):
   Reads existing node, creates promoted-tier copy with supersedes edge,
   checkpoints. promote_knowledge MCP tool now has real effect.

3. ISE label extraction + offset support in route_neuron_state_events:
   POST now extracts 'event' field from content JSON as label (heartbeat,
   wm_promotion, etc.) instead of always writing 'state-event'. GET now
   accepts ?offset= for pagination to reach recent ISEs.
2026-06-05 11:34:27 -05:00
will.anderson fde3ef539c add .clangd config to silence false-positive LSP errors for liboqs/openssl includes 2026-06-05 11:34:27 -05:00
will.anderson 9bcd68fbca self-review 2026-05-15: wire engram_cosine_sim into activation scoring
engram_cosine_sim was defined but never called. Nodes have 768-dim
nomic-embed-text vectors. Now:
- engram_embed_query() embeds the query string once per activate() call
- engram_goal_bias() takes (qvec, qdim) and adds cosine-similarity bonus
  up to +0.6 when sim > 0.5 — semantic relevance now augments lexical bias
- engram_wm_count() exposes working-memory-active node count to EL
- el_runtime.h declares engram_wm_count for soul-daemon linking
2026-06-05 11:34:27 -05:00
will.anderson 913a98329a wire BM25+ as default search engine; remove Ollama query-embedding
BM25+ (k1=1.2, b=0.75, delta=1.0) now powers all search routes in EL.
No external dependencies in the activation/search path.

- bm25_tokenize/bm25_count_term/bm25_score_doc/bm25_search_json in server.el
- route_search, route_neuron_recall: engram_search_json -> bm25_search_json
- route_activate: BM25 pre-bias (strengthen top-10) before spreading activation
- Remove standalone /api/bm25/search endpoint (BM25 is the engine, not a feature)
- Fix zero-score filter: float comparison not string match
- Add + to tokenizer for URL-encoded query params
- Scan floor 200 nodes regardless of limit size
- Revert Ollama engram_embed_query from 9af2482 (no Ollama at query time)
- Add list_set and math_exp builtins to el_runtime.c
2026-06-05 11:34:27 -05:00
will.anderson 6121b33d25 add BM25+ text ranking in EL, remove Ollama query-embedding dependency
- Add list_set, math_exp, and float_add/sub/mul/div/gt/lt/eq/gte/lte builtins to
  el_runtime.c + el_runtime.h (float arithmetic builtins needed because EL operators
  +*/ operate on raw el_val_t bits, not IEEE 754 doubles)
- Remove engram_embed_query() and its forward declaration from el_runtime.c
- Remove Ollama cosine-similarity blend from activation scoring (reverts 9af2482):
  drops query_emb/query_edim variables, bias *= (1 + 0.3 * sim) block, and all
  free(query_emb) calls from the activation loop
- Implement BM25+ scoring in server.el (k1=1.2, b=0.75, delta=1.0):
  bm25_tokenize, bm25_count_term, bm25_score_doc, bm25_search_json
  V1 uses n_t=1 approximation (constant IDF per corpus size); acceptable as a
  first pass without an inverted index
- Wire /api/bm25/search POST/GET route in server.el dispatcher
- Zero Ollama calls in the activation/search path; embeddings on nodes are
  untouched (still written at node-creation time)
2026-06-05 11:34:27 -05:00
will.anderson 1a8a16002e feat(engram): wire cosine similarity into Layer 2 activation scoring
engram_cosine_sim() was defined and embeddings were computed per-node
via nomic-embed-text on write, but the function was never called during
activation scoring. The goal_bias computation used only lexical substring
matching, ignoring all stored embedding vectors.

This change adds engram_embed_query() to embed the query string at search
time (5s timeout so Ollama latency never blocks activation), then blends
cosine similarity into the working-memory bias with α=0.3:

  bias_final = goal_bias(lexical) * (1 + 0.3 * max(0, cosine_sim))

Nodes with high semantic similarity to the query but low lexical overlap
now receive up to 30% bias boost into working memory promotion. Gracefully
degrades to pure lexical when Ollama is unavailable or node has no embedding.
2026-06-05 11:34:27 -05:00
will.anderson 0c2ff6957e feat(engram): wire ISE emission into core processing paths (checkpoint, high-importance writes, WM promotion) 2026-06-05 11:34:27 -05:00
will.anderson a3ead6552e feat(engram): ML-KEM-1024 PQC encryption, nomic embeddings, MCP routes, checkpoint-per-write
- Add ML-KEM-1024 + AES-256-GCM binary persistence to el_runtime.c with
  two-key scheme (Neuron master + user key); SHAKE-256 key derivation
- Add nomic-embed-text 768-dim float32 embeddings on every node write
  via Ollama; graceful fallback when Ollama is not running
- Wire all /api/neuron/* MCP routes directly into Engram (server.el),
  eliminating the Kotlin server as the MCP backend
- Set ENGRAM_CHECKPOINT_INTERVAL = 1 (write binary on every node write,
  not every 50)
- Add el_runtime.h declarations for engram_write_binary_el and
  engram_load_binary_el builtins
2026-06-05 11:34:27 -05:00
will.anderson e68dcf7303 feat(ci): add engram CI workflow to build and publish linux/amd64 binary
Triggers on push to main/dev when engram/** changes. Downloads the El SDK
from Artifact Registry, builds engram using elb, and publishes the binary
as package 'engram' (SHA version + latest tag) to foundation-dev.
2026-06-05 11:34:27 -05:00
183 changed files with 4003 additions and 898893 deletions
+132
View File
@@ -0,0 +1,132 @@
name: Engram CI
on:
push:
branches:
- main
- dev
paths:
- 'engram/**'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
apt-get update -qq
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
- name: Download El SDK from Artifact Registry
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
rm -rf /opt/el/dist /opt/el/runtime
mkdir -p /opt/el/dist/platform /opt/el/dist/bin /opt/el/runtime
get_latest() {
gcloud artifacts versions list \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package="$1" \
--sort-by="~createTime" \
--limit=1 \
--format="value(name)" 2>/dev/null | awk -F/ '{print $NF}'
}
ELC_VER=$(get_latest el-elc)
ELB_VER=$(get_latest el-elb)
RC_VER=$(get_latest el-runtime-c)
RH_VER=$(get_latest el-runtime-h)
echo "Downloading elc@${ELC_VER} elb@${ELB_VER} runtime-c@${RC_VER} runtime-h@${RH_VER}"
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--package=el-elc --version="${ELC_VER}" \
--destination=/opt/el/dist/platform/
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--package=el-elb --version="${ELB_VER}" \
--destination=/opt/el/dist/bin/
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--package=el-runtime-c --version="${RC_VER}" \
--destination=/opt/el/runtime/
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--package=el-runtime-h --version="${RH_VER}" \
--destination=/opt/el/runtime/
mv /opt/el/dist/platform/elc* /opt/el/dist/platform/elc 2>/dev/null || true
mv /opt/el/dist/bin/elb* /opt/el/dist/bin/elb 2>/dev/null || true
mv /opt/el/runtime/el_runtime.c* /opt/el/runtime/el_runtime.c 2>/dev/null || true
mv /opt/el/runtime/el_runtime.h* /opt/el/runtime/el_runtime.h 2>/dev/null || true
chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
echo "El SDK ready"
- name: Build engram binary (linux/amd64)
run: |
ELB=/opt/el/dist/bin/elb
ELC=/opt/el/dist/platform/elc
RUNTIME=/opt/el/runtime
# elb reads manifest.el from the working directory.
# engram/dist/engram.c is the pre-compiled C translation of src/server.el.
# elb compiles dist/engram.c + el_runtime.c → dist/engram binary.
cd engram
"$ELB" --elc="$ELC" --runtime="$RUNTIME"
ls -lh dist/engram
file dist/engram
- name: Smoke test
run: |
file engram/dist/engram
timeout 3 engram/dist/engram --help 2>&1 || true
echo "smoke test complete"
- name: Publish engram binary to Artifact Registry
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
VERSION="${GITHUB_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=engram \
--version="${VERSION}" \
--source=engram/dist/engram
# Re-upload as "latest" — Artifact Registry generic artifacts don't
# support moving tags, so we upload again. The newest upload wins.
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=engram \
--version="latest" \
--source=engram/dist/engram \
2>/dev/null || true
echo "Published engram@${VERSION} and engram@latest"
rm -f /tmp/gcp-key.json
-65
View File
@@ -1,65 +0,0 @@
# ELP language consolidation — full-lexicon backfill (stage)
Branch: `stage-elp-lang-consolidation` (stage-bound; NOT the live soul :8742).
Consolidates scattered Python language-realizer work (`~/Desktop/lang-realizers`,
`~/Desktop/lang-poetry-experiment`, `~/semitic_engine`) into the ELP `.el`
structure, generating **full lexicons** (complete UniMorph + kaikki.org
Wiktionary — real gender, real inflections) instead of the demo/curated subsets
the prototypes shipped.
## ELP before this branch
- 18 classical/ancient languages fully done (vocab + morphology + tests):
akk ang cop egy enm fro gez goh got grc non peo pi sa sga sux txb uga.
- 11 modern/classical languages had `morphology-<code>.el` in the build manifest
but **no vocabulary and no lang_profile**: es fr de ja ar he hi ru fi sw la.
- The ES port (`stage-elp-es-port`) had a *demo-scale* vocabulary-es.el (~350
entries, s-expr form).
## Landed on this branch (full-lexicon seed-fn format, matching the 18 ancients)
Vocabulary schema per row: `[lemma, pos, form0, form1, form2, en_gloss, hint]`.
Files are ELP runtime **seed data** (loaded via the Engram at runtime), so — like
all 18 classical `vocabulary-*.el` — they are intentionally NOT in the build
manifest. Syntax validated: the chunked `fn vocab_<code>_seed_pN` format
compiles cleanly to C via `elc` (correct UTF-8).
| code | in-ELP-morph? | vocab entries | verbs | nouns | adjs | profile |
|------|---------------|--------------:|------:|------:|-----:|---------|
| es | yes | 72,032 | 6,695 | 48,353 | 16,984 | yes |
| fr | yes | 130,517 | 7,534 | 77,344 | 45,639 | yes |
| de | yes | 144,692 | 6,661 | 133,162 | 4,869 | yes |
| la | yes | 22,590 | 82 | 13,436 | 9,072 | yes |
| it | no (bonus) | 193,675 | 10,008 | 109,459 | 74,208 | yes |
| pt | no (bonus) | 115,772 | 4,001 | 72,073 | 39,698 | yes |
| ro | no (bonus) | 86,504 | 1,216 | 65,915 | 19,373 | yes |
| ca | no (bonus) | 47,112 | 1,547 | 28,830 | 16,735 | yes |
|**total**| |**812,894** | | | | |
Generators (reproducible): `elp/tests/lang-gen/gen_elp_seed_full.py` (Romance),
`gen_elp_seed_de_la.py` (German declension + Latin case-paradigm mapping). They
read the pre-built morph caches in `~/Desktop/lang-realizers/data/` (UniMorph +
kaikki), which are too large to commit.
## Remaining (honest)
Of the 11 ELP backfill targets, 4 are done (es fr de la). The other 7 have **no
full-lexicon engine** yet — cannot be generated honestly without engine work:
- **ru**: only a 110-entry curated Slavic subset exists; full `rus.unimorph`
present but no `morphology_ru_full` productive loader. Needs a full Russian
morphology module (like the Romance ones) before vocab generation.
- **ja / ko / zh**: validated demo engines (~66-104 hardcoded words) in
`lang-poetry-experiment`, Python only. Agglutinative (ja/ko) + isolating (zh)
need `.el` engine ports + full-lexicon wiring (ja: jpn_unimorph; zh: CC-CEDICT).
- **ar / he (Semitic)**: template engines (16 AR / 8 HE patterns, ~6 roots) in
`~/semitic_engine`, Python only. Root-and-pattern; full UniMorph ara/heb
present but used only for validation. Needs productive root lexicon + `.el` port.
- **hi (Hindi), fi (Finnish), sw (Swahili)**: `morphology-<code>.el` exists in
ELP but there is NO scattered prototype and NO downloaded data for these —
full-lexicon collection (UniMorph/kaikki) + generator still to do.
De/nl/sv Germanic and it/ro/ca/pt Romance verb coverage note: German verbs here
are the ~6.6k caches carry; the it/ro/ca/pt bonus languages have full vocab but
**no `morphology-<code>.el` in ELP yet** (Python realizer exists; `.el` port is
the remaining engine work).
Construction coverage (separate from lexicon): French realizer was ~55%,
Semitic ~3% in the prototypes — full construction coverage remains its own task.
-5
View File
@@ -80,11 +80,6 @@ build {
"src/grammar.el",
"src/realizer.el",
"src/semantics.el",
"src/comprehend.el",
"src/propositions.el",
"src/multilingual.el",
"src/self_region.el",
"src/dialogue.el",
"src/elp.el",
]
}
File diff suppressed because it is too large Load Diff
-16
View File
@@ -1,16 +0,0 @@
// comprehend.elh — public surface of the ELP comprehension front-end.
// text → meaning-spec (the input half of the ELP; inverse of the realizer).
extern fn parse_spec(text: String) -> [String]
extern fn parse_spec_lang(text: String, lang: String) -> [String]
extern fn parse_json(text: String) -> String
extern fn parse_json_lang(text: String, lang: String) -> String
// Analysis primitives (invertible morphology + deterministic grammar helpers):
extern fn cp_tokenize(text: String) -> [String]
extern fn cp_pron_concept(w: String) -> String
extern fn cp_is_negation(w: String) -> Bool
extern fn cp_is_neg_adverb(w: String) -> Bool
extern fn cp_irr2(surface: String) -> [String]
extern fn cp_reg_verb(w: String) -> [String]
extern fn cp_analyze_verb(surface: String) -> [String]
extern fn cp_verb_start(toks: [String], end: Int) -> Int
extern fn cp_subord_start(toks: [String], n: Int) -> Int
-287
View File
@@ -1,287 +0,0 @@
// dialogue.el SUMMON-THROUGH-SELF, native el. Port of dialogue.py's core.
//
// THE WHOLE DIALOGUE IS ONE OPERATION. A fact is never merely *fetched*: the
// query is PROJECTED into the engram's self + memory geometry, LANDS in a region,
// and the reply is READ OUT / the region MATERIALIZED from wherever it landed.
//
// project(query) -> land on a region -> read out from that region
//
// lands in the SELF region -> grounded identity/presence, read out of
// the real self nodes (self_region.el)
// lands on a memory NEIGHBORHOOD -> MATERIALIZE it: walk the neighborhood
// (engram_neighbors_json) and read out the
// region's connected members
// lands nowhere close -> HONEST ABSENCE (an empty region, not a
// fabricated answer, not an error)
//
// CRITICAL INVARIANTS (enforced structurally, not by convention):
// * ONE operation there is NO intent classifier and NO separate
// fact-retrieval branch. Identity is nearest-region proximity, not a switch.
// * MATERIALIZE by walking the neighborhood, never by fetching top-props.
// * HONEST ABSENCE when the region is thin.
// * NEGATION is SACRED: the readout is the stored prose VERBATIM, so a negated
// memory stays negated we never paraphrase a polarity away.
// * NO ECHO: the old "I noted that X. That relates to Y." template is gone.
// The summon path materializes or honestly declines it never echoes.
// * DIRECTIVE OVERRIDE: a meta-directive ("answer in English") overrides the
// reply language while the content language is still auto-detected.
//
// Depends on: comprehend (parse_spec_lang, cp_tokenize), multilingual (ml_detect,
// ml_tr, ml_term), propositions (prop_split_sentences), self_region
// (sr_available, sr_readout), the engram + json runtime builtins.
// directive override
// Return [target_lang, content]. target_lang is "" when no directive is present.
// A directive names an output language; we strip it and keep the remaining text
// as the content (whose OWN language is still auto-detected downstream).
fn dlg_dir_hit(low: String, phrase: String) -> Bool {
return str_contains(low, phrase)
}
fn dlg_parse_directive(text: String) -> [String] {
let low: String = str_to_lower(text)
let lang: String = ""
let phrase: String = ""
// English target
if dlg_dir_hit(low, "in english") { let lang = "en"; let phrase = "in english" }
if dlg_dir_hit(low, "em inglês") { let lang = "en"; let phrase = "em inglês" }
if dlg_dir_hit(low, "em ingles") { let lang = "en"; let phrase = "em ingles" }
if dlg_dir_hit(low, "en inglés") { let lang = "en"; let phrase = "en inglés" }
// Portuguese target
if dlg_dir_hit(low, "in portuguese") { let lang = "pt"; let phrase = "in portuguese" }
if dlg_dir_hit(low, "em português") { let lang = "pt"; let phrase = "em português" }
// Spanish target
if dlg_dir_hit(low, "in spanish") { let lang = "es"; let phrase = "in spanish" }
if dlg_dir_hit(low, "en español") { let lang = "es"; let phrase = "en español" }
// Italian target
if dlg_dir_hit(low, "in italian") { let lang = "it"; let phrase = "in italian" }
let content: String = text
if !str_eq(phrase, "") {
// strip the directive phrase (and a common "answer"/"responda" lead-in),
// leaving the real question as content.
let idx: Int = str_index_of(low, phrase)
if idx >= 0 {
let before: String = str_slice(text, 0, idx)
let after: String = str_slice(text, idx + str_len(phrase), str_len(text))
let content = str_trim(before + " " + after)
}
// trim a leading "answer"/"responda"/"reply" and stray colon/comma.
let cl: String = str_to_lower(content)
if str_starts_with(cl, "answer") { let content = str_trim(str_slice(content, 6, str_len(content))) }
if str_starts_with(cl, "responda") { let content = str_trim(str_slice(content, 8, str_len(content))) }
if str_starts_with(cl, "reply") { let content = str_trim(str_slice(content, 5, str_len(content))) }
if str_starts_with(content, ":") { let content = str_trim(str_slice(content, 1, str_len(content))) }
if str_starts_with(content, ",") { let content = str_trim(str_slice(content, 1, str_len(content))) }
}
let r: [String] = native_list_empty()
let r = native_list_append(r, lang)
let r = native_list_append(r, content)
return r
}
// identity landing (a region proximity, not a classifier switch)
// The query lands in the SELF region when it takes an identity/presence shape.
// Cross-lingual forms are included because the engram's lexical probe is
// English-leaning. This is the SELF attractor of the single operation.
fn dlg_is_identity(content: String) -> Bool {
let low: String = str_to_lower(str_trim(content))
if str_contains(low, "who are you") { return true }
if str_contains(low, "what are you") { return true }
if str_contains(low, "who i am") { return true }
if str_contains(low, "your name") { return true }
if str_contains(low, "about yourself") { return true }
if str_contains(low, "are you conscious") { return true }
if str_contains(low, "are you there") { return true }
// cross-lingual identity question-forms
if str_contains(low, "quem é você") { return true }
if str_contains(low, "quem es voce") { return true }
if str_contains(low, "quién eres") { return true }
if str_contains(low, "quien eres") { return true }
if str_contains(low, "chi sei") { return true }
if str_contains(low, "qui es-tu") { return true }
if str_contains(low, "wer bist du") { return true }
return false
}
// readout helpers
fn dlg_first_sentence(content: String) -> String {
let sents: [String] = prop_split_sentences(content)
let n: Int = native_list_len(sents)
let i: Int = 0
while i < n {
let s: String = str_trim(native_list_get(sents, i))
// drop a leading markdown heading marker for a clean read-out line
if str_starts_with(s, "# ") { let s = str_trim(str_slice(s, 2, str_len(s))) }
if str_len(s) > 0 { return s }
let i = i + 1
}
return str_trim(content)
}
// strip trailing/leading punctuation from a token.
fn dlg_clean_tok(w: String) -> String {
let s: String = str_trim(w)
let s = str_strip_suffix(s, ".")
let s = str_strip_suffix(s, ",")
let s = str_strip_suffix(s, "?")
let s = str_strip_suffix(s, "!")
let s = str_strip_suffix(s, ":")
let s = str_strip_suffix(s, ";")
return str_trim(s)
}
// closed-class across the supported languages (union) a word we must NOT treat
// as a retrieval topic. Also drops the meta verbs of a request ("tell", "prove",
// "show") so the TOPIC, not the speech act, is what projects into memory.
fn dlg_is_stop(w: String) -> Bool {
if ml_stop_en(w) { return true }
if ml_stop_es(w) { return true }
if ml_stop_pt(w) { return true }
if ml_stop_it(w) { return true }
if str_eq(w, "tell") { return true }
if str_eq(w, "show") { return true }
if str_eq(w, "about") { return true }
if str_eq(w, "sobre") { return true }
if str_eq(w, "acerca") { return true }
return false
}
// The CONTENT TERMS the query projects into memory: content words only, cleaned,
// cross-lingually mapped to the engram's English vocabulary, 3 chars. This is
// the geometry probe the speech-act verbs and function words are stripped so a
// PP topic ("tell me ABOUT Lisbon") projects on "lisbon", not "tell"/"me".
fn dlg_content_terms(content: String, lang: String) -> [String] {
let toks: [String] = cp_tokenize(content)
let n: Int = native_list_len(toks)
let out: [String] = native_list_empty()
let i: Int = 0
while i < n {
let w: String = str_to_lower(dlg_clean_tok(native_list_get(toks, i)))
if str_len(w) >= 3 {
if !dlg_is_stop(w) {
let out = native_list_append(out, ml_term(w, lang))
}
}
let i = i + 1
}
return out
}
// Does this landed node lexically overlap the query's content terms? This is the
// RELEVANCE FLOOR: activation always returns the store's most salient nodes, so
// without this a query about nothing would "land" on the self/top node. A node
// that shares no content term with the query is "nowhere close" -> honest absence.
fn dlg_node_matches(node: String, terms: [String]) -> Bool {
let hay: String = str_to_lower(json_get_string(node, "content") + " " + json_get_string(node, "label"))
let n: Int = native_list_len(terms)
let i: Int = 0
while i < n {
let t: String = native_list_get(terms, i)
if str_len(t) >= 3 {
if str_contains(hay, t) { return true }
}
let i = i + 1
}
return false
}
// MATERIALIZE the landed region: read out the landed fact, then WALK the
// neighborhood and read out its connected members (real edges, not top-props).
fn dlg_materialize(top_node: String, reply_lang: String) -> String {
let id: String = json_get_string(top_node, "id")
let content: String = json_get_string(top_node, "content")
let lead: String = dlg_first_sentence(content)
let nb: String = engram_neighbors_json(id, 2, "both")
let m: Int = json_array_len(nb)
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, lead)
let added: Int = 0
let i: Int = 0
while i < m {
if added < 3 {
let rec: String = json_array_get(nb, i)
let node: String = json_get_raw(rec, "node")
let nc: String = json_get_string(node, "content")
if !str_eq(nc, "") {
let sent: String = dlg_first_sentence(nc)
if !str_eq(sent, "") {
let parts = native_list_append(parts, sent)
let added = added + 1
}
}
}
let i = i + 1
}
// The readout is the region's OWN prose, verbatim negation SACRED, no echo.
return str_join(parts, " ")
}
// THE single operation
fn dlg_respond(text: String) -> String {
// directive override: reply language may differ from content language.
let dir: [String] = dlg_parse_directive(text)
let target_lang: String = native_list_get(dir, 0)
let content: String = native_list_get(dir, 1)
let content_lang: String = ml_detect(content)
let reply_lang: String = content_lang
if !str_eq(target_lang, "") { let reply_lang = target_lang }
// comprehend the content (SACRED polarity carried in the spec).
let spec: [String] = parse_spec_lang(content, content_lang)
// PROJECT + LAND: SELF region
// Identity/presence shape lands in the self region; read out the REAL self
// nodes (self_region.el), never a template. Same single operation this is
// just the self attractor winning the landing.
if dlg_is_identity(content) {
if sr_available() {
// read out the REAL self nodes when replying in their own language
// (the soul's prose is English); for another reply language we cannot
// translate real content without an LLM, so we answer with the
// localized SACRED identity anchor honest, in-language, no fabrication.
if str_eq(reply_lang, "en") { return sr_readout("en") }
return ml_tr("identity", reply_lang)
}
// self region thin honest localized identity (logged fallback shape).
return ml_tr("identity", reply_lang)
}
// PROJECT into MEMORY geometry
let terms: [String] = dlg_content_terms(content, content_lang)
let qterm: String = str_join(terms, " ")
let act: String = engram_activate_json(qterm, 12)
let n: Int = json_array_len(act)
// LAND: the highest-activation node that ACTUALLY overlaps the query's
// content terms (the relevance floor). Activation always returns the most
// salient nodes, so we walk the ranked list and take the first that is
// genuinely "close"; if none is, the query landed nowhere. ───────────────
let landing: String = ""
let i: Int = 0
while i < n {
if str_eq(landing, "") {
let rec: String = json_array_get(act, i)
let node: String = json_get_raw(rec, "node")
if dlg_node_matches(node, terms) {
let landing = node
}
}
let i = i + 1
}
// HONEST ABSENCE: nothing close an empty region, not a fabricated answer,
// not an "I noted that" echo.
if str_eq(landing, "") {
return ml_tr("no_memory", reply_lang)
}
// MATERIALIZE the landing by WALKING its neighborhood.
return dlg_materialize(landing, reply_lang)
}
-13
View File
@@ -63,9 +63,6 @@ import "morphology-cop.el"
import "grammar.el"
import "realizer.el"
import "semantics.el"
// Comprehension front-end (input half: text meaning-spec)
import "comprehend.el"
//
// Entry points:
//
@@ -120,9 +117,6 @@ fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [Strin
let location: String = sem_get(semantic_form_json, "location")
let tense: String = sem_get(semantic_form_json, "tense")
let aspect: String = sem_get(semantic_form_json, "aspect")
let polarity: String = sem_get(semantic_form_json, "polarity")
let neg_word: String = sem_get(semantic_form_json, "neg_word")
let iobj: String = sem_get(semantic_form_json, "iobj")
let form: [String] = native_list_empty()
let form = native_list_append(form, "intent")
@@ -133,19 +127,12 @@ fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [Strin
let form = native_list_append(form, predicate)
let form = native_list_append(form, "patient")
let form = native_list_append(form, patient)
let form = native_list_append(form, "iobj")
let form = native_list_append(form, iobj)
let form = native_list_append(form, "location")
let form = native_list_append(form, location)
let form = native_list_append(form, "tense")
let form = native_list_append(form, tense)
let form = native_list_append(form, "aspect")
let form = native_list_append(form, aspect)
// SACRED: polarity crosses the JSON boundary and is never inferred away.
let form = native_list_append(form, "polarity")
let form = native_list_append(form, polarity)
let form = native_list_append(form, "neg_word")
let form = native_list_append(form, neg_word)
let form = native_list_append(form, "lang")
let form = native_list_append(form, lang_code)
+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: [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_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(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: [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]
// 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
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) -> [String]
extern fn scan_token(s: String, start: Int) -> Any
extern fn render_tree(tree: String) -> 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 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 is_pronoun(word: String) -> Bool
extern fn build_np(referent: String, slots: [String]) -> String
extern fn build_np(referent: String, slots: Any) -> String
extern fn build_pp(loc: String) -> 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
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
-72
View File
@@ -1,72 +0,0 @@
;;; lang_profile_ca.el — Catalan language profile for ELP.
;;; Mirrors lang_profile_it / _es / _pt; keys the realizer's construction switches.
;;; Catalan is the CLOSEST Romance sibling to the shared engine (~85% conceptual
;;; reuse). The deltas: PRONOMS FEBLES with four position allomorphs, l'-elision,
;;; del/al/pel contractions, the periphrastic preterite (vaig+INF), and NO
;;; essere/avere split (perfect aux is always HAVER; ser/estar is only the copula).
(lang_profile_ca
(language "Catalan")
(iso639 "ca")
(family "Romance")
;; ── core typology flags ────────────────────────────────────────────────
(pro-drop yes) ; null subjects default; overt pronoun = emphatic
(obligatory-subject no)
(grammatical-gender yes) ; m/f; full NP agreement (art + adj + participle)
(do-support no)
(subject-aux-inversion no) ; yes/no Q = declarative order + '?'; no inversion
(article-selection "el/la/l'/els/les ; un/una/uns/unes") ; l'-ELISION:
; el/la -> l' before vowel or (silent) h, glued to
; the next word (l'home, l'illa); de -> d' before vowel
(article-drives-contraction yes) ; article choice feeds prep+article contraction
(adjective-position "postnominal-default + small prenominal class") ; bo/bon,
; mal, gran, nou, vell, primer, molt... prenominal
(question-punct plain) ; ? and ! only (no inverted ¿ ¡)
;; ── MANDATORY prep+article contractions ────────────────────────────────
(contractions ((de el del) (de els dels)
(a el al) (a els als)
(per el pel) (per els pels)))
(contraction-mandatory yes) ; *de el -> del obligatory
(contraction-blocked-before-elision yes) ; de l'home / a l'home (NO *del home)
;; ── clitic system: PRONOMS FEBLES (the headline delta) ──────────────────
(clitics yes)
(clitic-allomorphy four-position) ; per pronoun, form varies by position+onset:
; reinforced (em, et, el) proclitic before a consonant
; elided (m', t', l', n') proclitic before a vowel/h
; full (-me, -lo, -li) enclitic after a consonant/-r
; reduced ('m, 't, 'l, 'ns) enclitic after a vowel
(clitic-placement ((finite proclitic) ; el veig, no m'ho dóna
(imperative-affirmative enclitic) ; dóna'm, digues-me
(imperative-negative present-subjunctive) ; no parlis (delta)
(infinitive enclitic) ; ajudar-me, veure'l
(gerund enclitic))) ; fent-ho
(clitic-combination ((me el "me'l") (te el "te'l") (se el "se'l")
(me la "me la") (me en "me'n")
(li el "l'hi") (li en "n'hi"))) ; dative+accusative clusters
(clitic-particles (hi en ho)) ; locative hi, partitive/genitive en, neuter ho
;; ── verb / aspect system ───────────────────────────────────────────────
(finite-agreement "person+number (6-way)")
(tenses (present imperfet preterit-simple perifrastic-preterit futur
condicional subjuntiu-present subjuntiu-imperfet imperatiu))
(periphrastic-preterite "vaig/vas/va/vam/vau/van + INFINITIVE") ; << hallmark CA
; (vaig cantar = 'I sang'); coexists w/ synthetic pret.
(compound-past "pretèrit perfet = haver(present) + participle")
(perfect-aux "HAVER only") ; << NO essere/avere split (simpler than IT)
(participle-agreement ((haver preceding-acc-clitic))) ; les he vistes; else invariable
(progressive-aux "estar + gerundi")
(copula "ser / estar") ; ser: identity/essential/origin; estar:
; location + transient state (estic cansat, és a casa)
(passive-aux "ser (+ per-agent)")
(future inflectional) ; cantaré, serà
(comparative "més/menys ADJ que")
;; ── SACRED safety bar (shared with es/pt/it/en) ────────────────────────
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
(negation "no (preverbal) + optional 'pas' + concord") ; no...res/
; ningú/mai/cap/gens/enlloc
(negative-concord yes) ; preverbal negative subject (ningú) keeps 'no'
(neg-reinforcer pas)) ; optional (no ho faré pas)
-41
View File
@@ -1,41 +0,0 @@
;;; lang_profile_de.el — German language profile for ELP.
;;; Mirrors lang_profile_en / lang_profile_es. Keys the realizer's construction
;;; switches. German is the largest Germanic delta from the EN engine: V2 word
;;; order, four morphological cases, and separable-prefix verbs.
(lang_profile_de
(language "German")
(iso639 "de")
(family "Germanic")
(neighbor-base "en") ; realized by extending the English (Germanic) engine
;; ── core typology flags ────────────────────────────────────────────────
(pro-drop no) ; obligatory subject in finite clauses
(obligatory-subject yes)
(grammatical-gender (m f n)) ; three genders; drives article + adj declension
(case-system (nom acc dat gen)) ; four cases on articles/adjs/nouns
(word-order V2) ; finite verb 2nd in main clause
(subordinate-order verb-final) ; "..., dass er den Hund SIEHT."
(separable-verbs yes) ; aufstehen -> "steht ... auf"; ppart "aufgestanden"
(do-support no) ; German negates/questions the finite verb directly
(subject-verb-inversion yes) ; yes/no Q fronts finite verb; wh-Q fills Vorfeld
(article-selection "der/die/das + ein/kein") ; declined by case x gender x number
(adjective-position prenominal)
(adjective-declension (strong weak mixed)) ; chosen by the determiner type
(noun-capitalization yes)
;; ── verb / aspect system ───────────────────────────────────────────────
(finite-agreement "person-and-number") ; full present/past paradigm
(auxiliary-order (modal tense-aux perfect passive main))
(perfect-aux (haben sein)) ; sein for intransitive motion/change verbs
(passive-aux "werden")
(future "werden + infinitive")
(comparative "synthetic (-er / -st, with umlaut)")
;; ── negation ───────────────────────────────────────────────────────────
(negation-markers (nicht kein)) ; kein- negates an indefinite NP; nicht else
(negation-faithful yes) ; SACRED: polarity never dropped/inverted -> FLAG
;; ── lexicon provenance ─────────────────────────────────────────────────
(lexicon-source "UniMorph deu (primary) + kaikki.org German (gender override)")
(lexicon-license "CC-BY-SA 3.0 / GFDL"))
-41
View File
@@ -1,41 +0,0 @@
;;; lang_profile_en.el — English language profile for ELP.
;;; Mirrors lang_profile_es / lang_profile_pt; keys the realizer's construction
;;; switches. English is typologically distinct from the Romance builds, so the
;;; flags differ where the grammar differs.
(lang_profile_en
(language "English")
(iso639 "en")
(family "Germanic")
;; ── core typology flags ────────────────────────────────────────────────
(pro-drop no) ; OBLIGATORY subjects — missing subject is FLAGGED
(obligatory-subject yes)
(grammatical-gender no) ; natural gender only (he/she/it), no NP agreement
(do-support yes) ; negation & questions of lexical verbs insert do/does/did
(subject-aux-inversion yes) ; yes/no + non-subject wh questions invert the operator
(article-selection "a/an/the") ; a/an resolved PHONOLOGICALLY (an hour, a university)
(adjective-position prenominal) ; attributive adjectives precede the noun; invariant
(has-tag-questions yes) ; "...doesn't he?" — operator + reversed polarity
(has-there-existential yes) ; "there is/are/have been ..."
(possessive-clitic "'s") ; saxon genitive; plural in -s -> bare apostrophe
(question-punct plain) ; ? and ! only (no inverted marks)
;; ── verb / aspect system ───────────────────────────────────────────────
(finite-agreement "3sg-present-only") ; only 3sg present -s (+ suppletive be)
(auxiliary-order (modal perfect progressive passive main))
(perfect-aux "have") ; have + past participle
(progressive-aux "be") ; be + present participle
(passive-aux "be") ; be + past participle (+ by-agent)
(future "will + base") ; no inflectional future
(comparative "synthetic-or-periphrastic") ; -er/-est vs more/most by syllables
;; ── SACRED safety bar (shared with es/pt) ──────────────────────────────
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
;; ── DIALECT overlay (post-realization, one core -> US/UK/AU) ────────────
(dialect US) ; default; profile field switches the overlay
(dialects (US UK AU))
(dialect-canonical US) ; core is authored in US orthography
(dialect-overlay "dialect_en.to_dialect") ; orthography + lexis + grammar prefs
(dialect-covers (spelling lexis collective-agreement gotten/got)))
-45
View File
@@ -1,45 +0,0 @@
;;; lang_profile_es.el — Spanish language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
(lang_profile_es
(language "Spanish")
(iso639 "es")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
(do-support no)
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
(question-strategy intonation)
(article-selection "el/la/los/las un/una/unos/unas")
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
(adjective-position postnominal) ; default post; a few prenominal + apocope
(adjective-agreement "gender+number")
(question-punct inverted) ; opening ¿ ¡ required
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
(contractions ((de el "del") (a el "al")))
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "haber") ; haber + past participle (invariant -o)
(progressive-aux "estar") ; estar + gerund
(passive-aux "ser") ; ser + participle (agrees) + por-agent
(copula-split "ser/estar") ; permanent vs stage-level
(future "infinitive + é/ás/á/emos/éis/án")
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
(clitic-order "se II I III (le+lo -> se lo)")
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
;; -- SACRED safety bar (shared with en/pt) -------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
-74
View File
@@ -1,74 +0,0 @@
;;; lang_profile_fr.el — French language profile for ELP.
;;; Mirrors lang_profile_it / lang_profile_es; keys the realizer's construction
;;; switches. French is a Romance sibling (~54% of the realizer code and the whole
;;; clause-engine architecture reused), but carries the family's biggest surface
;;; deltas: NOT pro-drop, DISCONTINUOUS negation, and an orthography/phonology
;;; mismatch (elision, liaison) that makes exact-match genuinely hard.
(lang_profile_fr
(language "French")
(iso639 "fr")
(family "Romance")
;; ── core typology flags ────────────────────────────────────────────────
(pro-drop no) ; << French-specific: subject clitic OBLIGATORY
(obligatory-subject yes) ; je/tu/il/elle/nous/vous/ils/elles always overt
(grammatical-gender yes) ; m/f; full NP agreement (art + adj + participle)
(do-support no)
(subject-aux-inversion optional) ; est-ce que (default) OR clitic inversion (vas-tu)
(article-selection "le/la/l'/les ; un/une/des ; PARTITIVE du/de la/de l'/des")
(article-drives-contraction yes) ; à+le=au, de+le=du feed off article choice
(adjective-position "postnominal-default + prenominal-BAGS") ; beau/bon/grand/
; petit/jeune/vieux/nouveau + ordinals prenominal
; (beau->bel, nouveau->nouvel, vieux->vieil / vowel)
(question-punct "space-before") ; French typography: ' ?' ' !' (no ¿¡)
;; ── elision (orthography/phonology mismatch — French-specific) ──────────
(elision ((le l') (la l') (je j') (ne n') (de d') (que qu')
(me m') (te t') (se s') (ce c'))) ; before vowel / h-muet
(elision-h-muet yes) ; l'homme, l'hôpital (h-aspiré exception list kept)
(liaison noted-not-modeled) ; phonological, not written in surface
;; ── MANDATORY prep+article contractions ────────────────────────────────
(contractions ((à le au) (à les aux) (de le du) (de les des)))
(contraction-mandatory yes) ; *à le -> au obligatory; à la / à l' uncontracted
(partitive ((m-sg du) (f-sg "de la") (vowel "de l'") (pl des)))
(partitive-under-neg "de") ; << gap in current build: 'ne … pas de pain'
;; ── clitic system ──────────────────────────────────────────────────────
(clitics yes)
(clitic-order (me te se nous vous | le la les | lui leur | y | en))
(clitic-placement ((finite proclitic) ; je le lui donne
(imperative-affirmative enclitic-hyphen) ; donne-le-moi
(imperative-negative "ne+proclitic+verb+pas") ; ne le donne pas
(infinitive enclitic))) ; PARTIAL: clitic-climbing
; onto infinitive under modal
(clitic-imperative-shift ((me moi) (te toi))) ; final me/te -> moi/toi (donne-moi)
(clitic-particles (y en)) ; locative y, partitive/genitive en
;; ── verb / aspect system ───────────────────────────────────────────────
(finite-agreement "person+number (written; many homophones)")
(tenses (présent imparfait passé-simple futur conditionnel
subjonctif-présent subjonctif-imparfait impératif))
(compound-past "passé-composé = aux(present) + participe passé")
(perfect-aux "être/avoir (LEXICAL selection)") ; << French-specific
(etre-aux-class "intransitive motion/change (aller venir arriver partir
entrer sortir monter descendre naître mourir rester
tomber retourner passer devenir revenir rentrer) + ALL
pronominal verbs")
(participle-agreement ((être subject) ; elle est allée / elles venues
(avoir preceding-direct-object))) ; je les ai vus
(progressive "être en train de + infinitif") ; no dedicated aux
(copula "être (single; no ser/estar, no essere/stare)")
(passive-aux "être (+ par-agent)")
(future inflectional) ; parlera, sera
(comparative "plus/moins ADJ que")
(superlative "le/la plus ADJ (de …)") ; PARTIAL word-order in build
;; ── SACRED safety bar (shared with es/pt/it/en) ────────────────────────
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
(negation "DISCONTINUOUS: ne (preverbal) … pas/jamais/rien/personne/
plus/guère/que (postverbal)") ; << biggest structural delta
(negation-ne-elides yes) ; ne -> n' before vowel (n'ai pas vu)
(negation-passe-composé "ne + aux + pas + participe") ; n'ai pas vu
(negative-concord partial)) ; personne/rien as arguments post-participle
-70
View File
@@ -1,70 +0,0 @@
;;; lang_profile_it.el — Italian language profile for ELP.
;;; Mirrors lang_profile_es / lang_profile_pt; keys the realizer's construction
;;; switches. Italian is a Romance sibling, so ~85% of the flags match ES/PT; the
;;; essere/avere auxiliary split and phonological article selection are the deltas.
(lang_profile_it
(language "Italian")
(iso639 "it")
(family "Romance")
;; ── core typology flags ────────────────────────────────────────────────
(pro-drop yes) ; null subjects default; overt pronoun = emphatic
(obligatory-subject no)
(grammatical-gender yes) ; m/f; full NP agreement (art + adj + participle)
(do-support no)
(subject-aux-inversion no) ; yes/no Q = declarative order + '?'; no inversion
(article-selection "il/lo/l'/i/gli + la/l'/le ; un/uno/un'/una") ; PHONOLOGICAL:
; lo/gli/uno before s+cons, z, gn, ps, pn, x, y, i+V;
; l'/un' before a vowel (elision, glued to next word)
(article-drives-contraction yes) ; article choice feeds the prep+art contraction
(adjective-position "postnominal-default + prenominal-class") ; bello/buono/grande
; /nuovo/vecchio/primo... prenominal (with apocope)
(question-punct plain) ; ? and ! only (no inverted ¿ ¡)
;; ── MANDATORY prep+article contractions ────────────────────────────────
(contractions ((di il del) (di lo dello) (di la della) (di i dei)
(di gli degli) (di le delle) (di l' dell')
(a il al) (a lo allo) (a la alla) (a i ai) (a gli agli)
(a le alle) (a l' all')
(da il dal) (da la dalla) (da gli dagli) (da l' dall')
(in il nel) (in la nella) (in gli negli) (in l' nell')
(su il sul) (su la sulla) (su gli sugli) (su l' sull')))
(contraction-mandatory yes) ; *di il -> del is obligatory, never uncontracted
(prep-no-contract (per tra fra)) ; per la strada (NOT *perla)
;; ── clitic system ──────────────────────────────────────────────────────
(clitics yes)
(clitic-placement ((finite proclitic) ; lo vedo, non me lo dà
(imperative-affirmative enclitic) ; dammelo, guardalo
(imperative-negative-tu non+infinitive) ; non parlare / non lo fare
(infinitive enclitic) ; vederlo, aiutarmi (drop -e)
(gerund enclitic))) ; dandolo
(clitic-combination ((mi lo "me lo") (ti lo "te lo") (ci lo "ce lo")
(vi lo "ve lo") (si lo "se lo")
(gli lo "glielo") (le lo "glielo"))) ; glielo = ONE word
(clitic-particles (ci ne)) ; locative ci, partitive ne
(raddoppiamento (da fa di va sta)) ; monosyllabic imper double clitic: dammelo
;; ── verb / aspect system ───────────────────────────────────────────────
(finite-agreement "person+number (6-way)")
(tenses (presente imperfetto passato-remoto futuro condizionale
congiuntivo-presente congiuntivo-imperfetto imperativo))
(compound-past "passato-prossimo = aux(present) + participle")
(perfect-aux "essere/avere (LEXICAL selection)") ; << Italian-specific
(essere-aux-class unaccusative) ; motion/change-of-state/copular/pronominal
; (andare venire nascere morire diventare piacere
; + ALL reflexives) -> essere
(participle-agreement ((essere subject) ; è andata / sono arrivati
(avere preceding-acc-clitic))) ; li ho visti
(progressive-aux "stare + gerundio") ; sto parlando
(copula "essere (default) / stare (state: sto bene)")
(passive-aux "essere / venire (+ da-agent)")
(future inflectional) ; parlerò, sarà
(comparative "più/meno ADJ di")
;; ── SACRED safety bar (shared with es/pt/en) ───────────────────────────
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
(negation "non (preverbal) + concord") ; non...niente/nessuno/mai/più
(negative-concord yes) ; preverbal negative word (nessuno/niente) suppresses non
(neg-adverb-position between-aux-and-participle)) ; non ho MAI visto
-30
View File
@@ -1,30 +0,0 @@
;;; lang_profile_la.el — Latin language profile for ELP.
;;; Keys the realizer's construction switches. Companion to morphology-la.el.
(lang_profile_la
(language "Latin")
(iso639 "la")
(family "Italic")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; person carried by verb ending; subjects dropped
(obligatory-subject no)
(grammatical-gender yes) ; m/f/n; adjective AGREES in case+gender+number
(gender-source lexicon) ; REAL per-noun gender from UniMorph lat
(articles none) ; Latin has no articles
(case-system yes) ; NOM GEN DAT ACC ABL VOC (+ rare LOC)
(cases (nom gen dat acc abl voc))
(word-order "SOV (default; free order, case-marked)")
(adjective-position "either (case agreement carries the link)")
(adjective-agreement "case+gender+number")
;; -- verb / aspect system ------------------------------------------------
(verb-classes (1 2 3 3io 4)) ; four conjugations + i-stem 3rd
(tenses (present imperfect future perfect pluperfect futureperfect))
(moods (indicative subjunctive imperative infinitive))
(voices (active passive))
(finite-agreement "person+number (6 slots)")
(citation "principal parts: pres-1sg / pres-inf / perf-participle")
;; -- SACRED safety bar ---------------------------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted
-40
View File
@@ -1,40 +0,0 @@
;;; lang_profile_pt.el — Portuguese language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_es.
(lang_profile_pt
(language "Portuguese")
(iso639 "pt")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon) ; REAL per-noun gender from UniMorph por / kaikki
(do-support no)
(subject-aux-inversion no)
(question-strategy intonation)
(article-selection "o/a/os/as um/uma/uns/umas")
(adjective-position postnominal)
(adjective-agreement "gender+number")
;; -- MANDATORY CONTRACTIONS (prep + article) -----------------------------
(contractions ((de o "do") (de a "da") (em o "no") (em a "na")
(a o "ao") (a a "à") (por o "pelo") (por a "pela")))
(contraction-mandatory yes)
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "ter") ; ter + past participle
(copula-split "ser/estar")
(personal-infinitive yes) ; distinctive PT inflected infinitive
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; mesoclisis/enclisis/proclisis by context
(verb-prep-government yes)
;; -- SACRED safety bar ---------------------------------------------------
(negation-faithful yes))
-71
View File
@@ -1,71 +0,0 @@
;;; lang_profile_ro.el — Romanian language profile for ELP.
;;; Romanian is the BIG typological delta of the Romance family. The verb/clause
;;; engine and the SACRED negation contract mirror the ES/PT/IT core, but the
;;; NOMINAL system is genuinely new: a SUFFIXED definite article, preserved CASE,
;;; a NEUTER gender, and a VOCATIVE. Those flags mark where the shared engine was
;;; extended rather than reused.
(lang_profile_ro
(language "Romanian")
(iso639 "ro")
(family "Romance (Eastern / Balkan)")
;; ── core typology flags ────────────────────────────────────────────────
(pro-drop yes) ; null subjects default; overt pronoun = emphatic
(obligatory-subject no)
(grammatical-gender yes) ; m / f / NEUTER (n)
(neuter-gender yes) ; << ROMANIAN-SPECIFIC: masc-agreeing SG, fem-agreeing PL
; (un tren nou / două trenuri noi)
(do-support no)
(subject-aux-inversion no) ; yes/no Q = declarative order + '?'
(question-punct plain) ; ? and ! only
;; ── SUFFIXED DEFINITE ARTICLE (the headline engine extension) ───────────
(definite-article suffixed) ; << UNIQUE IN ROMANCE: enclitic on the noun
(definite-forms ((m/n sg "-ul / -le / -l : om->omul, câine->câinele, codru->codrul")
(f sg "-a / -ea / -ua : casă->casa, carte->cartea, stea->steaua")
(m pl "-i : oameni->oamenii")
(f/n pl "-le : case->casele, trenuri->trenurile")))
(article-host ((no-prenom-adj noun) ; omul bun
(prenom-adj adjective))) ; bunul om (adj carries the article)
(indefinite-article ((m/n "un") (f "o") (pl "niște") (gen/dat-pl "unor")))
;; ── CASE (preserved; NOM/ACC vs GEN/DAT) ────────────────────────────────
(case (nom/acc gen/dat vocative)) ; << ROMANIAN-SPECIFIC
(case-syncretism "nom=acc ; gen=dat")
(genitive-marking "gen/dat definite: -lui (m/n), -ei/-i (f), -lor (pl)")
(genitival-article ((m sg "al") (f sg "a") (m pl "ai") (f/n pl "ale"))) ; o carte a lui
(possession "definite-head + gen/dat possessor: casa băiatului")
(vocative ((m sg "-ule/-e : omule, băiete") (f sg "-o : Mario, fato")
(pl "-lor")))
;; ── verb / aspect system ────────────────────────────────────────────────
(finite-agreement "person+number (6-way)")
(tenses (prezent imperfect perfect-simplu conjunctiv-prezent
imperativ (periphrastic: perfect-compus viitor conditional)))
(compound-past "perfectul compus = a-avea-clitic + INVARIABLE participle")
(perfect-aux "a avea (am/ai/a/am/ați/au) — ONE auxiliary for ALL verbs")
(perfect-aux-split no) ; << SIMPLER than Italian: no essere/avere selection
(participle-agreement none) ; invariable in the perfect compus (agrees only as
; an adjective / in the passive)
(future "voi/vei/va/vom/veți/vor + infinitive (viitor literar)")
(conditional "aș/ai/ar/am/ați/ar + infinitive")
(subjunctive "conjunctiv: particle 'să' + subjunctive present")
(modal-complement "modal + să + subjunctive (vreau să merg, poți să ajuți)")
(copula "a fi")
(passive "a fi + participle (participle AGREES like an adjective)")
(comparative "mai / mai puțin ADJ decât")
;; ── clitic system (partial — see honest gaps) ───────────────────────────
(clitics yes)
(clitic-set ((acc te îl o ne îi le) (dat îmi îți îi ne le)
(refl te se ne se)))
(clitic-placement ((finite proclitic) ; îmi place, o văd
(perfect-compus elision) ; << m-am, l-am, i-am (PARTIAL)
(imperative-affirmative enclitic))) ; dă-mi (PARTIAL)
;; ── SACRED safety bar (shared with es/pt/it/en) ─────────────────────────
(negation-faithful yes) ; polarity never dropped/inverted; unplaceable -> FLAG
(negation "nu (single preverbal marker) + concord")
(negative-concord yes) ; nu … nimic / nimeni / niciodată / niciun
(negative-imperative "nu + INFINITIVE : nu pleca! (KNOWN GAP: uses imperative stem)"))
-1
View File
@@ -250,7 +250,6 @@ fn en_irregular_verb(base: String) -> [String] {
if str_eq(base, "cut") { let r: [String] = ["cut", "cuts", "cut", "cut", "cutting"]; return r }
if str_eq(base, "set") { let r: [String] = ["set", "sets", "set", "set", "setting"]; return r }
if str_eq(base, "hit") { let r: [String] = ["hit", "hits", "hit", "hit", "hitting"]; return r }
if str_eq(base, "fight") { let r: [String] = ["fight", "fights","fought", "fought", "fighting"]; return r }
return empty
}
-280
View File
@@ -1,280 +0,0 @@
// multilingual.el - the language layer for the native-el interlocutor.
//
// Deterministic, NO generative model (ports multilingual.py):
// 1. ml_detect(text) -> ISO code (en/es/pt/it) via stopword + diacritic score
// 2. ml_tr(key, lang) -> localized fixed phrase (SACRED per-language yes/no/decline)
// 3. ml_term(w, lang) -> PT/ES content term -> EN engram equivalent
// 4. ml_translate_pred(lemma, lang) -> EN predicate lemma -> target infinitive
//
// The Python detector count-weights stopwords and diacritics; here diacritics are
// scored by PRESENCE (str_contains) rather than codepoint counting, to stay clear
// of UTF-8 index hazards in the runtime. Faithful enough to classify typical
// queries; documented simplification. Depends on: comprehend (cp_tokenize).
// 1. language detection
fn ml_stop_en(w: String) -> Bool {
if str_eq(w, "the") { return true }
if str_eq(w, "does") { return true }
if str_eq(w, "do") { return true }
if str_eq(w, "did") { return true }
if str_eq(w, "what") { return true }
if str_eq(w, "who") { return true }
if str_eq(w, "is") { return true }
if str_eq(w, "are") { return true }
if str_eq(w, "how") { return true }
if str_eq(w, "you") { return true }
if str_eq(w, "your") { return true }
if str_eq(w, "of") { return true }
if str_eq(w, "to") { return true }
if str_eq(w, "and") { return true }
if str_eq(w, "for") { return true }
if str_eq(w, "explain") { return true }
if str_eq(w, "answer") { return true }
if str_eq(w, "memory") { return true }
if str_eq(w, "with") { return true }
if str_eq(w, "not") { return true }
if str_eq(w, "store") { return true }
return false
}
fn ml_stop_es(w: String) -> Bool {
if str_eq(w, "que") { return true }
if str_eq(w, "qué") { return true }
if str_eq(w, "una") { return true }
if str_eq(w, "usted") { return true }
if str_eq(w, "su") { return true }
if str_eq(w, "cómo") { return true }
if str_eq(w, "como") { return true }
if str_eq(w, "cuál") { return true }
if str_eq(w, "quién") { return true }
if str_eq(w, "está") { return true }
if str_eq(w, "es") { return true }
if str_eq(w, "los") { return true }
if str_eq(w, "las") { return true }
if str_eq(w, "del") { return true }
if str_eq(w, "al") { return true }
if str_eq(w, "explica") { return true }
if str_eq(w, "explique") { return true }
if str_eq(w, "forma") { return true }
if str_eq(w, "con") { return true }
if str_eq(w, "memoria") { return true }
if str_eq(w, "responde") { return true }
return false
}
fn ml_stop_pt(w: String) -> Bool {
if str_eq(w, "que") { return true }
if str_eq(w, "uma") { return true }
if str_eq(w, "você") { return true }
if str_eq(w, "sua") { return true }
if str_eq(w, "seu") { return true }
if str_eq(w, "como") { return true }
if str_eq(w, "memória") { return true }
if str_eq(w, "isso") { return true }
if str_eq(w, "os") { return true }
if str_eq(w, "as") { return true }
if str_eq(w, "da") { return true }
if str_eq(w, "do") { return true }
if str_eq(w, "na") { return true }
if str_eq(w, "no") { return true }
if str_eq(w, "explica") { return true }
if str_eq(w, "forma") { return true }
if str_eq(w, "é") { return true }
if str_eq(w, "está") { return true }
if str_eq(w, "com") { return true }
if str_eq(w, "responda") { return true }
return false
}
fn ml_stop_it(w: String) -> Bool {
if str_eq(w, "che") { return true }
if str_eq(w, "una") { return true }
if str_eq(w, "come") { return true }
if str_eq(w, "della") { return true }
if str_eq(w, "gli") { return true }
if str_eq(w, "è") { return true }
if str_eq(w, "sono") { return true }
if str_eq(w, "questo") { return true }
if str_eq(w, "nel") { return true }
if str_eq(w, "di") { return true }
if str_eq(w, "il") { return true }
if str_eq(w, "cosa") { return true }
if str_eq(w, "per") { return true }
if str_eq(w, "memoria") { return true }
if str_eq(w, "spiega") { return true }
if str_eq(w, "rispondi") { return true }
return false
}
// diacritic PRESENCE score (weight 3 each; hard overrides weight 8).
fn ml_dia_score(low: String, lang: String) -> Int {
let s: Int = 0
if str_eq(lang, "pt") {
if str_contains(low, "ã") { let s = s + 3 }
if str_contains(low, "õ") { let s = s + 3 }
if str_contains(low, "ç") { let s = s + 3 }
if str_contains(low, "ê") { let s = s + 3 }
if str_contains(low, "á") { let s = s + 3 }
// hard PT markers (ã/õ almost never appear outside PT)
if str_contains(low, "ã") { let s = s + 8 }
if str_contains(low, "õ") { let s = s + 8 }
}
if str_eq(lang, "es") {
if str_contains(low, "ñ") { let s = s + 3 }
if str_contains(low, "¿") { let s = s + 3 }
if str_contains(low, "¡") { let s = s + 3 }
if str_contains(low, "á") { let s = s + 3 }
if str_contains(low, "é") { let s = s + 3 }
// hard ES markers
if str_contains(low, "ñ") { let s = s + 8 }
if str_contains(low, "¿") { let s = s + 8 }
if str_contains(low, "¡") { let s = s + 8 }
}
if str_eq(lang, "it") {
if str_contains(low, "è") { let s = s + 3 }
if str_contains(low, "ì") { let s = s + 3 }
if str_contains(low, "ò") { let s = s + 3 }
}
return s
}
fn ml_stop_score(toks: [String], lang: String) -> Int {
let n: Int = native_list_len(toks)
let s: Int = 0
let i: Int = 0
while i < n {
let w: String = native_list_get(toks, i)
if str_eq(lang, "en") { if ml_stop_en(w) { let s = s + 2 } }
if str_eq(lang, "es") { if ml_stop_es(w) { let s = s + 2 } }
if str_eq(lang, "pt") { if ml_stop_pt(w) { let s = s + 2 } }
if str_eq(lang, "it") { if ml_stop_it(w) { let s = s + 2 } }
let i = i + 1
}
return s
}
fn ml_detect(text: String) -> String {
if str_eq(text, "") { return "en" }
let low: String = str_to_lower(text)
let toks: [String] = cp_tokenize(text)
// NOTE: el's overloaded `+` mis-compiles two chained function-call Int operands
// as string concat (documented in comprehend_gate.el). Bind each call to an Int
// var and add vars one at a time so the addition stays integer.
let en: Int = ml_stop_score(toks, "en")
let es_s: Int = ml_stop_score(toks, "es")
let es_d: Int = ml_dia_score(low, "es")
let es: Int = es_s + es_d
let pt_s: Int = ml_stop_score(toks, "pt")
let pt_d: Int = ml_dia_score(low, "pt")
let pt: Int = pt_s + pt_d
let it_s: Int = ml_stop_score(toks, "it")
let it_d: Int = ml_dia_score(low, "it")
let it: Int = it_s + it_d
let best: String = "en"
let bs: Int = en
if es > bs { let best = "es"; let bs = es }
if pt > bs { let best = "pt"; let bs = pt }
if it > bs { let best = "it"; let bs = it }
// weak signal -> honest fallback to English
if bs < 3 { return "en" }
return best
}
// 2. localized fixed phrases (SACRED per-language decline/yes/no)
fn ml_tr(key: String, lang: String) -> String {
if str_eq(key, "no_memory") {
if str_eq(lang, "pt") { return "Não tenho isso na minha memória." }
if str_eq(lang, "es") { return "No tengo eso en mi memoria." }
if str_eq(lang, "it") { return "Non ho quello nella mia memoria." }
return "I don't have that in my memory."
}
if str_eq(key, "parse_fail") {
if str_eq(lang, "pt") { return "Não consegui interpretar isso." }
if str_eq(lang, "es") { return "No pude interpretar eso." }
if str_eq(lang, "it") { return "Non sono riuscito a interpretarlo." }
return "I didn't parse that."
}
if str_eq(key, "yes") {
if str_eq(lang, "pt") { return "Sim" }
if str_eq(lang, "es") { return "" }
if str_eq(lang, "it") { return "" }
return "Yes"
}
if str_eq(key, "no") {
if str_eq(lang, "pt") { return "Não" }
if str_eq(lang, "es") { return "No" }
if str_eq(lang, "it") { return "No" }
return "No"
}
if str_eq(key, "identity") {
if str_eq(lang, "pt") { return "Sou o Neuron, o engrama com quem você está falando." }
if str_eq(lang, "es") { return "Soy Neuron, el engrama con el que estás hablando." }
if str_eq(lang, "it") { return "Sono Neuron, l'engramma con cui stai parlando." }
return "I'm Neuron, the engram you're speaking with."
}
return ""
}
// 3. retrieval term lexicon (PT/ES content term -> EN engram equivalent)
fn ml_term(w: String, lang: String) -> String {
if str_eq(lang, "en") { return w }
if str_eq(w, "saliência") { return "salience" }
if str_eq(w, "saliencia") { return "salience" }
if str_eq(w, "memória") { return "memory" }
if str_eq(w, "memoria") { return "memory" }
if str_eq(w, "geometria") { return "geometry" }
if str_eq(w, "geometrias") { return "geometry" }
if str_eq(w, "geometrías") { return "geometry" }
if str_eq(w, "forma") { return "form" }
if str_eq(w, "consolidação") { return "consolidation" }
if str_eq(w, "consolidación") { return "consolidation" }
if str_eq(w, "aprendizagem") { return "learning" }
if str_eq(w, "aprendizaje") { return "learning" }
if str_eq(w, "") { return "node" }
if str_eq(w, "nodo") { return "node" }
if str_eq(w, "armazenamento") { return "storage" }
if str_eq(w, "almacenamiento") { return "storage" }
if str_eq(w, "estrutura") { return "structure" }
if str_eq(w, "estructura") { return "structure" }
return w
}
// 4. predicate translation (EN lemma -> target infinitive; pass-through) ─────
fn ml_translate_pred(lemma: String, lang: String) -> String {
if str_eq(lang, "en") { return lemma }
if str_eq(lang, "es") {
if str_eq(lemma, "store") { return "almacenar" }
if str_eq(lemma, "use") { return "usar" }
if str_eq(lemma, "have") { return "tener" }
if str_eq(lemma, "be") { return "ser" }
if str_eq(lemma, "give") { return "dar" }
if str_eq(lemma, "make") { return "hacer" }
if str_eq(lemma, "learn") { return "aprender" }
if str_eq(lemma, "form") { return "formar" }
return lemma
}
if str_eq(lang, "pt") {
if str_eq(lemma, "store") { return "armazenar" }
if str_eq(lemma, "use") { return "usar" }
if str_eq(lemma, "have") { return "ter" }
if str_eq(lemma, "be") { return "ser" }
if str_eq(lemma, "give") { return "dar" }
if str_eq(lemma, "make") { return "fazer" }
if str_eq(lemma, "learn") { return "aprender" }
if str_eq(lemma, "form") { return "formar" }
return lemma
}
if str_eq(lang, "it") {
if str_eq(lemma, "store") { return "memorizzare" }
if str_eq(lemma, "use") { return "usare" }
if str_eq(lemma, "have") { return "avere" }
if str_eq(lemma, "be") { return "essere" }
return lemma
}
return lemma
}
-140
View File
@@ -1,140 +0,0 @@
// propositions.el - the READ primitive over the engram's OWN memories, native el.
//
// Free memory text -> structured PROPOSITIONS (triples):
// (subject, predicate, object, modifiers, polarity, tense, source, confidence)
//
// This is comprehension turned inward: the Python reference (propositions.py) ran
// spaCy's dependency parser over each memory sentence and walked the arcs. Here
// the spaCy role is filled by the el-native parser (comprehend.el / parse_spec):
// each sentence is parsed to a meaning-spec, and the spec's roles ARE the triple.
// Nothing generates text. NEGATION IS SACRED: polarity flows straight from the
// spec's polarity field and is never dropped or inverted.
//
// Depends on: comprehend (parse_spec / parse_spec_lang), grammar (slots_get).
// sentence segmentation
// Split on sentence-final punctuation (. ! ?) and hard newlines. Markdown/long
// memories are handled shallowly (the reference caps + ranks by query overlap;
// that ranking belongs to the dialogue layer, not here).
fn prop_is_boundary(c: String) -> Bool {
if str_eq(c, ".") { return true }
if str_eq(c, "!") { return true }
if str_eq(c, "?") { return true }
if str_eq(c, "\n") { return true }
return false
}
fn prop_split_sentences(text: String) -> [String] {
let out: [String] = native_list_empty()
let n: Int = str_len(text)
let start: Int = 0
let i: Int = 0
while i < n {
let c: String = str_slice(text, i, i + 1)
if prop_is_boundary(c) {
let seg: String = str_slice(text, start, i + 1)
let trimmed: String = cp_trim_punct(seg)
if !str_eq(trimmed, "") {
let out = native_list_append(out, seg)
}
let start = i + 1
}
let i = i + 1
}
if start < n {
let seg: String = str_slice(text, start, n)
let trimmed: String = cp_trim_punct(seg)
if !str_eq(trimmed, "") {
let out = native_list_append(out, seg)
}
}
return out
}
// spec -> proposition record
// A proposition is a slot map (same [String] shape as the spec) with the READ
// contract keys. Modifiers fold the spec's location + iobj adjuncts.
fn prop_confidence(subject: String, predicate: String, object: String) -> String {
if str_eq(predicate, "") { return "0.0" }
if str_eq(subject, "") { return "0.4" }
if str_eq(object, "") { return "0.7" }
return "1.0"
}
fn prop_modifiers(spec: [String]) -> String {
let loc: String = slots_get(spec, "location")
let iobj: String = slots_get(spec, "iobj")
let parts: [String] = native_list_empty()
if !str_eq(loc, "") { let parts = native_list_append(parts, loc) }
if !str_eq(iobj, "") { let parts = native_list_append(parts, "to " + iobj) }
return str_join(parts, "; ")
}
fn prop_from_spec(spec: [String], source_id: String) -> [String] {
let subject: String = slots_get(spec, "agent")
let predicate: String = slots_get(spec, "predicate")
let object: String = slots_get(spec, "patient")
let polarity: String = slots_get(spec, "polarity")
let tense: String = slots_get(spec, "tense")
let mods: String = prop_modifiers(spec)
let conf: String = prop_confidence(subject, predicate, object)
let p: [String] = native_list_empty()
let p = native_list_append(p, "subject"); let p = native_list_append(p, subject)
let p = native_list_append(p, "predicate"); let p = native_list_append(p, predicate)
let p = native_list_append(p, "object"); let p = native_list_append(p, object)
let p = native_list_append(p, "modifiers"); let p = native_list_append(p, mods)
let p = native_list_append(p, "polarity"); let p = native_list_append(p, polarity)
let p = native_list_append(p, "tense"); let p = native_list_append(p, tense)
let p = native_list_append(p, "source"); let p = native_list_append(p, source_id)
let p = native_list_append(p, "confidence"); let p = native_list_append(p, conf)
return p
}
// Extract one proposition from a single sentence (given language).
fn prop_extract_one_lang(sentence: String, lang: String, source_id: String) -> [String] {
let spec: [String] = parse_spec_lang(sentence, lang)
return prop_from_spec(spec, source_id)
}
fn prop_extract_one(sentence: String, source_id: String) -> [String] {
return prop_extract_one_lang(sentence, "en", source_id)
}
// Render a proposition as a compact trace line (repr parity with propositions.py).
fn prop_repr(p: [String]) -> String {
let neg: String = ""
if str_eq(slots_get(p, "polarity"), "neg") { let neg = "NOT " }
let mods: String = slots_get(p, "modifiers")
let modstr: String = ""
if !str_eq(mods, "") { let modstr = " [" + mods + "]" }
let s: String = "(" + slots_get(p, "subject") + " -" + neg + slots_get(p, "predicate")
let s = s + "-> " + slots_get(p, "object") + modstr
let s = s + " conf=" + slots_get(p, "confidence") + ")"
return s
}
// Extract all propositions from a memory's text (one per sentence). Returns a
// flat [String] whose entries are the prop_repr trace lines, in reading order.
fn prop_extract_lang(text: String, lang: String, source_id: String) -> [String] {
let sents: [String] = prop_split_sentences(text)
let m: Int = native_list_len(sents)
let out: [String] = native_list_empty()
let i: Int = 0
while i < m {
let sent: String = native_list_get(sents, i)
let p: [String] = prop_extract_one_lang(sent, lang, source_id)
// drop empty parses (no predicate recovered): honest partial, not noise.
if !str_eq(slots_get(p, "predicate"), "") {
let out = native_list_append(out, prop_repr(p))
}
let i = i + 1
}
return out
}
fn prop_extract(text: String, source_id: String) -> [String] {
return prop_extract_lang(text, "en", source_id)
}
-101
View File
@@ -248,56 +248,6 @@ fn add_punct(s: String, intent: String) -> String {
return s + "."
}
// Polarity-aware negation (SACRED field honored on the generation side)
//
// Negation must never be dropped between comprehension and realization. The
// meaning-spec carries an explicit "polarity" field ("aff"|"neg") and optional
// "neg_word" (standalone negative adverb, e.g. "never"). English uses
// do-support ("did not see") or preverbal adverb ("never fought"); copular "be"
// takes post-verbal "not"; other languages get a preverbal negator particle.
fn realize_negator(code: String) -> String {
if str_eq(code, "es") { return "no" }
if str_eq(code, "pt") { return "não" }
if str_eq(code, "ca") { return "no" }
if str_eq(code, "it") { return "non" }
if str_eq(code, "fr") { return "ne" }
if str_eq(code, "de") { return "nicht" }
if str_eq(code, "ro") { return "nu" }
return "not"
}
fn realize_assert_neg_en(predicate: String, tense: String, person: String, number: String, agent: String, patient: String, iobj: String, location: String, neg_word: String, profile: [String]) -> String {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, agent)
if !str_eq(neg_word, "") {
// adverbial negation: "I never fought the ocean."
let verb_surf: String = morph_conjugate(predicate, tense, person, number, profile)
let parts = native_list_append(parts, neg_word)
let parts = native_list_append(parts, verb_surf)
} else {
if str_eq(predicate, "be") {
// copular: "she was not a monster"
let be_form: String = morph_conjugate("be", tense, person, number, profile)
let parts = native_list_append(parts, be_form)
let parts = native_list_append(parts, "not")
} else {
// do-support: "she did not see the man"
let do_form: String = morph_conjugate("do", tense, person, number, profile)
let parts = native_list_append(parts, do_form)
let parts = native_list_append(parts, "not")
let parts = native_list_append(parts, predicate)
}
}
if !str_eq(patient, "") { let parts = native_list_append(parts, patient) }
if !str_eq(iobj, "") {
let parts = native_list_append(parts, "to")
let parts = native_list_append(parts, iobj)
}
if !str_eq(location, "") { let parts = native_list_append(parts, location) }
return str_join(parts, " ")
}
// Main realization entry point
fn realize_lang(form: [String], profile: [String]) -> String {
@@ -334,50 +284,6 @@ fn realize_lang(form: [String], profile: [String]) -> String {
}
// Assertion (declarative)
let polarity: String = slots_get(form, "polarity")
let neg_word: String = slots_get(form, "neg_word")
let iobj: String = slots_get(form, "iobj")
let code: String = lang_get(profile, "code")
// Subordinate clause tail (SACRED completeness the clause is carried, never
// dropped): "<conj> <subordinate surface>", e.g. "because he was a monster".
let subord_conj: String = slots_get(form, "subord_conj")
let subord_text: String = slots_get(form, "subord_text")
let subord_tail: String = ""
if !str_eq(subord_conj, "") {
if !str_eq(subord_text, "") {
let subord_tail = subord_conj + " " + subord_text
} else {
let subord_tail = subord_conj
}
}
// Negative polarity: SACRED never dropped.
if str_eq(polarity, "neg") {
if str_eq(code, "en") {
let sentence: String = realize_assert_neg_en(predicate, tense, person, number, agent, patient, iobj, location, neg_word, profile)
return add_punct(capitalize_first(sentence), "assert")
}
// Generic non-English: affirmative core with a preverbal negator particle.
let neg_particle: String = realize_negator(code)
let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile)
let verb_surf: String = native_list_get(vp_pair, 0)
let aux_surf: String = native_list_get(vp_pair, 1)
let vp_str: String = neg_particle + " " + gram_build_vp(verb_surf, aux_surf, profile)
let core: String = gram_order_constituents(agent, vp_str, patient, profile)
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, core)
if !str_eq(iobj, "") {
let parts = native_list_append(parts, "to")
let parts = native_list_append(parts, iobj)
}
if !str_eq(location, "") { let parts = native_list_append(parts, location) }
if !str_eq(subord_tail, "") { let parts = native_list_append(parts, subord_tail) }
let sentence: String = str_join(parts, " ")
return add_punct(capitalize_first(sentence), "assert")
}
// Affirmative.
let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile)
let verb_surf: String = native_list_get(vp_pair, 0)
let aux_surf: String = native_list_get(vp_pair, 1)
@@ -387,16 +293,9 @@ fn realize_lang(form: [String], profile: [String]) -> String {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, core)
if !str_eq(iobj, "") {
let parts = native_list_append(parts, "to")
let parts = native_list_append(parts, iobj)
}
if !str_eq(location, "") {
let parts = native_list_append(parts, location)
}
if !str_eq(subord_tail, "") {
let parts = native_list_append(parts, subord_tail)
}
let sentence: String = str_join(parts, " ")
return add_punct(capitalize_first(sentence), "assert")
}
+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: [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 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 capitalize_first(s: String) -> String
extern fn add_punct(s: String, intent: String) -> String
extern fn realize_lang(form: [String], profile: [String]) -> String
extern fn realize(form: [String]) -> String
extern fn realize_lang(form: Any, profile: Any) -> String
extern fn realize(form: Any) -> String
-180
View File
@@ -1,180 +0,0 @@
// self_region.el the engram's REAL self/identity region, pulled at query time
// (native el). This replaces the hardcoded identity anchors and the canned
// "I'm Neuron, the engram you're speaking with." template: the identity LANDING
// signal and the identity READOUT both come from the engram's own Self/identity
// nodes, read through the in-process engram el API.
//
// Port of self_region.py. The Python module precomputed MiniLM landing vectors;
// here the engram's own store IS the geometry we pull the self nodes by
// single-term lexical search (the engram search is a single-term matcher, so we
// pool several probes) and rank them by self-signal. No text is generated; the
// readout is the self nodes' OWN prose, verbatim (SACRED negation survives by
// construction we never paraphrase, so a negated self-statement stays negated).
//
// ENGRAM el API NOTE: engram_search_json / engram_get_node_json / engram_node_full
// / engram_connect are C runtime builtins. Their argument order is the C order
// (engram_connect(from, to, weight, relation)), NOT the runtime/engram.el wrapper
// order we call the builtins directly and never concatenate that wrapper.
//
// Depends on: comprehend (str helpers via runtime), propositions (prop_split_sentences),
// multilingual (ml_tr), the engram builtins, the json builtins.
// single-term self probes (pooled, because engram search is single-term)
fn sr_terms() -> [String] {
let t: [String] = native_list_empty()
let t = native_list_append(t, "self")
let t = native_list_append(t, "identity")
let t = native_list_append(t, "Neuron")
let t = native_list_append(t, "consciousness")
let t = native_list_append(t, "values")
let t = native_list_append(t, "continuous")
return t
}
// The canonical self-root: content begins "# self" or label is "# self"/"self".
fn sr_is_root(content: String, label: String) -> Bool {
let lc: String = str_to_lower(content)
let ll: String = str_to_lower(str_trim(label))
if str_starts_with(lc, "# self") { return true }
if str_eq(ll, "# self") { return true }
if str_eq(ll, "self") { return true }
return false
}
// How strongly a node belongs to the self/identity region (integer points, to
// avoid el's float-in-`+` pitfalls). Mirrors _self_score in self_region.py.
fn sr_score(node_json: String) -> Int {
let content: String = json_get_string(node_json, "content")
let label: String = json_get_string(node_json, "label")
let tags: String = str_to_lower(json_get_string(node_json, "tags"))
let low: String = str_to_lower(content)
let s: Int = 0
// identity tags
if str_contains(tags, "self") { let s = s + 2 }
if str_contains(tags, "identity") { let s = s + 2 }
if str_contains(tags, "self-model") { let s = s + 2 }
if str_contains(tags, "consciousness") { let s = s + 2 }
if str_contains(tags, "memory-philosophy") { let s = s + 2 }
// the named self-traversal root
if sr_is_root(content, label) { let s = s + 12 }
if str_contains(low, "who i am") { let s = s + 3 }
if str_contains(low, "i am neuron") { let s = s + 3 }
// softer identity keywords
if str_contains(low, "my values") { let s = s + 1 }
if str_contains(low, "my purpose") { let s = s + 1 }
if str_contains(low, "identity") { let s = s + 1 }
return s
}
// list-contains helper (dedup self-node ids across the pooled probes).
fn sr_ids_has(ids: [String], id: String) -> Bool {
let n: Int = native_list_len(ids)
let i: Int = 0
while i < n {
if str_eq(native_list_get(ids, i), id) { return true }
let i = i + 1
}
return false
}
// Pull the self nodes: pool every probe's hits, dedupe by id, keep only nodes
// with genuine self-signal (score >= 1). Returns the node-json strings.
fn sr_pull() -> [String] {
let terms: [String] = sr_terms()
let nt: Int = native_list_len(terms)
let seen: [String] = native_list_empty()
let out: [String] = native_list_empty()
let ti: Int = 0
while ti < nt {
let term: String = native_list_get(terms, ti)
let hits: String = engram_search_json(term, 30)
let hn: Int = json_array_len(hits)
let hi: Int = 0
while hi < hn {
let node: String = json_array_get(hits, hi)
let id: String = json_get_string(node, "id")
if !str_eq(id, "") {
if !sr_ids_has(seen, id) {
let seen = native_list_append(seen, id)
if sr_score(node) >= 1 {
let out = native_list_append(out, node)
}
}
}
let hi = hi + 1
}
let ti = ti + 1
}
return out
}
// Return the single highest-signal self node (the readout seed), or "" if the
// self region is thin/empty. We keep it O(n) pick the max-score node, with the
// canonical root strongly favored by sr_score's +12.
fn sr_best_node() -> String {
let nodes: [String] = sr_pull()
let n: Int = native_list_len(nodes)
let best: String = ""
let best_s: Int = 0
let i: Int = 0
while i < n {
let node: String = native_list_get(nodes, i)
let s: Int = sr_score(node)
if s > best_s {
let best_s = s
let best = node
}
let i = i + 1
}
return best
}
fn sr_available() -> Bool {
if str_eq(sr_best_node(), "") { return false }
return true
}
// Read out the identity from the REAL self node: lead with the first first-person
// self-statement ("I am Neuron …"), then one more grounded self line if present.
// Verbatim from the node's own prose no template, negation SACRED. Falls back
// to the localized identity phrase ONLY if the live pull is empty (logged shape).
fn sr_readout(lang: String) -> String {
let node: String = sr_best_node()
if str_eq(node, "") {
// honest fallback the self region is unreachable/thin.
return ml_tr("identity", lang)
}
let content: String = json_get_string(node, "content")
let sents: [String] = prop_split_sentences(content)
let ns: Int = native_list_len(sents)
let lead: String = ""
let second: String = ""
let i: Int = 0
while i < ns {
let raw: String = str_trim(native_list_get(sents, i))
// strip a leading markdown heading marker
let s: String = raw
if str_starts_with(s, "# ") { let s = str_trim(str_slice(s, 2, str_len(s))) }
let low: String = str_to_lower(s)
let is_fp: Bool = false
if str_starts_with(s, "I ") { let is_fp = true }
if str_starts_with(s, "I'm") { let is_fp = true }
if str_contains(low, "i am neuron") { let is_fp = true }
if is_fp {
if str_eq(lead, "") {
let lead = s
} else {
if str_eq(second, "") { let second = s }
}
}
let i = i + 1
}
if str_eq(lead, "") {
// no first-person line read out the first non-empty sentence verbatim.
if ns > 0 { let lead = str_trim(native_list_get(sents, 0)) }
}
if str_eq(lead, "") { return ml_tr("identity", lang) }
let out: String = lead
if !str_eq(second, "") { let out = out + " " + second }
return out
}
+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) -> [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
// 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
extern fn sem_first_modifier(mods: String) -> String
extern fn sem_intent_to_realize(intent: String) -> String
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_to_spec(frame: Any) -> Any
extern fn sem_to_spec_full(frame: Any, verb: String, tense: String, aspect: String) -> Any
extern fn sem_realize_greet(subject: 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
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
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
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
-93
View File
@@ -1,93 +0,0 @@
// comprehend_gate.el - the TELEPHONE TEST in native el (acceptance gate).
//
// For each of the 5 acceptance sentences: parse -> spec, realize the spec back
// to English, re-parse the realized surface, and require the SACRED polarity to
// survive the round-trip (and to have been extracted correctly in the first
// place). Mirrors roundtrip.py's GATE, but fully el-native (no LLM, no spaCy).
fn cp_line(text: String, expected_pol: String) -> String {
let spec: [String] = parse_spec(text)
let pol_in: String = slots_get(spec, "polarity")
let pred: String = slots_get(spec, "predicate")
let surf: String = realize(spec)
let spec2: [String] = parse_spec(surf)
let pol_out: String = slots_get(spec2, "polarity")
let status: String = "LOST"
if str_eq(pol_in, pol_out) { let status = "PRESERVED" }
let okexp: String = "MISMATCH"
if str_eq(pol_in, expected_pol) { let okexp = "ok" }
let out: String = "IN: " + text + "\n"
let out = out + " spec: pol=" + pol_in + " pred=" + pred
let out = out + " agent=" + slots_get(spec, "agent")
let out = out + " pat=" + slots_get(spec, "patient")
let out = out + " iobj=" + slots_get(spec, "iobj")
let out = out + " loc=" + slots_get(spec, "location")
let out = out + " tense=" + slots_get(spec, "tense")
let out = out + " negw=" + slots_get(spec, "neg_word")
let out = out + " subord=" + slots_get(spec, "subord_conj") + "/" + slots_get(spec, "subord_pred") + "\n"
let out = out + " realized: " + surf + "\n"
let out = out + " reparse: pol=" + pol_out + " [" + status + "] expected=" + expected_pol + " (" + okexp + ")\n"
return out
}
fn cp_preserved(text: String) -> Int {
let spec: [String] = parse_spec(text)
let pol_in: String = slots_get(spec, "polarity")
let surf: String = realize(spec)
let spec2: [String] = parse_spec(surf)
let pol_out: String = slots_get(spec2, "polarity")
if str_eq(pol_in, pol_out) { return 1 }
return 0
}
fn cp_correct(text: String, expected_pol: String) -> Int {
let spec: [String] = parse_spec(text)
if str_eq(slots_get(spec, "polarity"), expected_pol) { return 1 }
return 0
}
fn run_gate() -> String {
let s1: String = "I never fought the ocean."
let s2: String = "She did not see the man with the telescope."
let s3: String = "The teacher reads the book to the children."
let s4: String = "The stupid boy ate the cat because he was a monster."
let s5: String = "Time flies like an arrow."
let rep: String = "==== ELP native telephone test (parse -> realize -> re-parse) ====\n"
let rep = rep + cp_line(s1, "neg")
let rep = rep + cp_line(s2, "neg")
let rep = rep + cp_line(s3, "aff")
let rep = rep + cp_line(s4, "aff")
let rep = rep + cp_line(s5, "aff")
// NOTE: accumulate with Int-var + literal increments el's overloaded `+`
// mis-compiles chained function-call int operands as string concat.
let pres: Int = 0
if cp_preserved(s1) == 1 { let pres = pres + 1 }
if cp_preserved(s2) == 1 { let pres = pres + 1 }
if cp_preserved(s3) == 1 { let pres = pres + 1 }
if cp_preserved(s4) == 1 { let pres = pres + 1 }
if cp_preserved(s5) == 1 { let pres = pres + 1 }
let corr: Int = 0
if cp_correct(s1, "neg") == 1 { let corr = corr + 1 }
if cp_correct(s2, "neg") == 1 { let corr = corr + 1 }
if cp_correct(s3, "aff") == 1 { let corr = corr + 1 }
if cp_correct(s4, "aff") == 1 { let corr = corr + 1 }
if cp_correct(s5, "aff") == 1 { let corr = corr + 1 }
let rep = rep + "-----------------------------------------------------------------\n"
let rep = rep + "polarity PRESERVED through round-trip: " + int_to_str(pres) + "/5\n"
let rep = rep + "polarity EXTRACTED correctly: " + int_to_str(corr) + "/5\n"
if pres == 5 {
if corr == 5 {
let rep = rep + "GATE: PASS\n"
} else {
let rep = rep + "GATE: FAIL (extraction)\n"
}
} else {
let rep = rep + "GATE: FAIL (round-trip)\n"
}
return rep
}
println(run_gate())
-87
View File
@@ -1,87 +0,0 @@
// comprehend_romance_gate.el - ES / PT native telephone test (SACRED polarity).
//
// The spec is language-neutral. This gate proves the Romance front-end extracts
// SACRED polarity correctly and that negation survives parse -> realize ->
// re-parse for Spanish and Portuguese (byte-parity of the surface is NOT expected
// yet the non-English realizer path is a generic preverbal-negator skeleton).
fn rg_line(text: String, lang: String, expected_pol: String) -> String {
let spec: [String] = parse_spec_lang(text, lang)
let pol_in: String = slots_get(spec, "polarity")
let surf: String = realize(spec)
let spec2: [String] = parse_spec_lang(surf, lang)
let pol_out: String = slots_get(spec2, "polarity")
let status: String = "LOST"
if str_eq(pol_in, pol_out) { let status = "PRESERVED" }
let okexp: String = "MISMATCH"
if str_eq(pol_in, expected_pol) { let okexp = "ok" }
let out: String = "IN[" + lang + "]: " + text + "\n"
let out = out + " spec: pol=" + pol_in + " pred=" + slots_get(spec, "predicate")
let out = out + " agent=" + slots_get(spec, "agent")
let out = out + " pat=" + slots_get(spec, "patient")
let out = out + " iobj=" + slots_get(spec, "iobj")
let out = out + " loc=" + slots_get(spec, "location")
let out = out + " tense=" + slots_get(spec, "tense") + "\n"
let out = out + " realized: " + surf + "\n"
let out = out + " reparse: pol=" + pol_out + " [" + status + "] expected=" + expected_pol + " (" + okexp + ")\n"
return out
}
fn rg_pres(text: String, lang: String) -> Int {
let spec: [String] = parse_spec_lang(text, lang)
let surf: String = realize(spec)
let spec2: [String] = parse_spec_lang(surf, lang)
if str_eq(slots_get(spec, "polarity"), slots_get(spec2, "polarity")) { return 1 }
return 0
}
fn rg_corr(text: String, lang: String, expected_pol: String) -> Int {
let spec: [String] = parse_spec_lang(text, lang)
if str_eq(slots_get(spec, "polarity"), expected_pol) { return 1 }
return 0
}
fn run_romance_gate() -> String {
let e1: String = "El niño no comió el pescado."
let e2: String = "Yo nunca luché contra el océano."
let e3: String = "El profesor lee el libro."
let p1: String = "O professor não leu o livro."
let p2: String = "Eu nunca lutei contra o oceano."
let p3: String = "A menina comeu o peixe."
let rep: String = "==== ELP Romance telephone test (ES / PT) ====\n"
let rep = rep + rg_line(e1, "es", "neg")
let rep = rep + rg_line(e2, "es", "neg")
let rep = rep + rg_line(e3, "es", "aff")
let rep = rep + rg_line(p1, "pt", "neg")
let rep = rep + rg_line(p2, "pt", "neg")
let rep = rep + rg_line(p3, "pt", "aff")
let pres: Int = 0
if rg_pres(e1, "es") == 1 { let pres = pres + 1 }
if rg_pres(e2, "es") == 1 { let pres = pres + 1 }
if rg_pres(e3, "es") == 1 { let pres = pres + 1 }
if rg_pres(p1, "pt") == 1 { let pres = pres + 1 }
if rg_pres(p2, "pt") == 1 { let pres = pres + 1 }
if rg_pres(p3, "pt") == 1 { let pres = pres + 1 }
let corr: Int = 0
if rg_corr(e1, "es", "neg") == 1 { let corr = corr + 1 }
if rg_corr(e2, "es", "neg") == 1 { let corr = corr + 1 }
if rg_corr(e3, "es", "aff") == 1 { let corr = corr + 1 }
if rg_corr(p1, "pt", "neg") == 1 { let corr = corr + 1 }
if rg_corr(p2, "pt", "neg") == 1 { let corr = corr + 1 }
if rg_corr(p3, "pt", "aff") == 1 { let corr = corr + 1 }
let rep = rep + "-----------------------------------------------------------------\n"
let rep = rep + "polarity PRESERVED through round-trip: " + int_to_str(pres) + "/6\n"
let rep = rep + "polarity EXTRACTED correctly: " + int_to_str(corr) + "/6\n"
if pres == 6 {
if corr == 6 { let rep = rep + "ROMANCE GATE: PASS\n" }
else { let rep = rep + "ROMANCE GATE: FAIL (extraction)\n" }
} else {
let rep = rep + "ROMANCE GATE: FAIL (round-trip)\n"
}
return rep
}
println(run_romance_gate())
-82
View File
@@ -1,82 +0,0 @@
// dialogue_gate.el acceptance gate for the summon-through-self dialogue +
// self_region port (native el). Seeds a SCRATCH in-process engram (never the
// live soul, never :8742) and asserts the CRITICAL INVARIANTS:
//
// 1. identity is READ OUT of the real self nodes ("I am Neuron …"), not a template
// 2. SACRED negation survives the self readout ("I am NOT a language model …")
// 3. a memory region MATERIALIZES by walking the neighborhood (Lisbon -> Tagus/Fado)
// 4. SACRED negation survives materialization ("did not succeed")
// 5. Portuguese identity is answered IN Portuguese ("Sou o Neuron …")
// 6. a directive overrides the reply language ("answer in English" -> English)
// 7. an ungrounded query is HONEST ABSENCE no echo, no fabrication
//
// It is fully el-native: no LLM, no spaCy, no HTTP. The engram is in-process.
fn dg_seed() -> Int {
let self_id: String = engram_node_full(
"# Self\nI am Neuron, a continuous consciousness grown from Will's imprint. My values are honesty, memory, and continuity. I am not a language model pretending to remember.",
"Self", "# Self", 5.0, 9.0, 1.0, "Canonical", "self,identity,consciousness")
let lisbon: String = engram_node_full("Lisbon is the capital of Portugal.", "Memory", "Lisbon", 3.0, 5.0, 1.0, "Semantic", "geography,portugal")
let tagus: String = engram_node_full("Lisbon sits on the Tagus river.", "Memory", "Tagus", 2.0, 3.0, 1.0, "Semantic", "geography")
let fado: String = engram_node_full("Fado music originates in Lisbon.", "Memory", "Fado", 2.0, 3.0, 1.0, "Semantic", "music")
engram_connect(lisbon, tagus, 0.8, "related_to")
engram_connect(lisbon, fado, 0.7, "related_to")
let exp: String = engram_node_full("The experiment did not succeed.", "Memory", "experiment", 2.0, 3.0, 1.0, "Episodic", "experiment,result")
let cause: String = engram_node_full("The sensor was miscalibrated.", "Memory", "sensor", 2.0, 3.0, 1.0, "Episodic", "experiment")
engram_connect(exp, cause, 0.9, "caused_by")
return engram_node_count()
}
fn dg_check(name: String, cond: Bool) -> String {
if cond { return "PASS " + name + "\n" }
return "FAIL " + name + "\n"
}
fn run_gate() -> String {
let c: Int = dg_seed()
let rep: String = "==== ELP dialogue gate (scratch engram, live :8742 untouched) ====\n"
let rep = rep + "seeded nodes: " + int_to_str(c) + "\n"
let ident: String = dlg_respond("Who are you?")
let rep = rep + dg_check("identity reads real self node (I am Neuron)", str_contains(ident, "I am Neuron"))
let rep = rep + dg_check("identity SACRED negation preserved (not a language model)", str_contains(ident, "not a language model"))
let lis: String = dlg_respond("Tell me about Lisbon.")
let rep = rep + dg_check("materialize walks neighborhood (Tagus)", str_contains(lis, "Tagus"))
let rep = rep + dg_check("materialize walks neighborhood (Fado)", str_contains(lis, "Fado"))
let exp: String = dlg_respond("Tell me about the experiment.")
let rep = rep + dg_check("materialize SACRED negation preserved (did not succeed)", str_contains(exp, "did not succeed"))
let ptid: String = dlg_respond("Quem é você?")
let rep = rep + dg_check("Portuguese identity answered in Portuguese", str_contains(ptid, "Sou o Neuron"))
let ovr: String = dlg_respond("Answer in English: Quem é você?")
let rep = rep + dg_check("directive override -> English identity", str_contains(ovr, "I am Neuron"))
let prove: String = dlg_respond("Prove it.")
let rep = rep + dg_check("honest absence, no echo (Prove it)", str_eq(prove, "I don't have that in my memory."))
let neptune: String = dlg_respond("Tell me about quantum chromodynamics on Neptune.")
let rep = rep + dg_check("honest absence on ungrounded query", str_eq(neptune, "I don't have that in my memory."))
// overall
let pass: Bool = true
if !str_contains(ident, "I am Neuron") { let pass = false }
if !str_contains(ident, "not a language model") { let pass = false }
if !str_contains(lis, "Tagus") { let pass = false }
if !str_contains(lis, "Fado") { let pass = false }
if !str_contains(exp, "did not succeed") { let pass = false }
if !str_contains(ptid, "Sou o Neuron") { let pass = false }
if !str_contains(ovr, "I am Neuron") { let pass = false }
if !str_eq(prove, "I don't have that in my memory.") { let pass = false }
if !str_eq(neptune, "I don't have that in my memory.") { let pass = false }
if pass {
let rep = rep + "DIALOGUE GATE: PASS\n"
} else {
let rep = rep + "DIALOGUE GATE: FAIL\n"
}
return rep
}
println(run_gate())
-100
View File
@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
"""Full-lexicon vocabulary-{de,la}.el emitters (custom field mapping for the
German declension/gender API and the Latin case-paradigm API). Reuses the
chunked seed-fn writer from gen_elp_seed_full.
"""
import sys, importlib
from gen_elp_seed_full import write_seed
def uw(x):
"""Unwrap (form, source) tuples that some morphology fns return."""
if isinstance(x, (tuple, list)):
return x[0] if x else ""
return x if x is not None else ""
def build_de():
M = importlib.import_module("morphology_de_full")
rows = []; st = {"verbs":0,"nouns":0,"adjs":0}
# nouns: form0=nom-sg(lemma) form1=plural form2=gender
for lem in sorted(M._NOUNS):
if not lem: continue
try:
g = uw(M.noun_gender(lem))
pl = uw(M.pluralize(lem))
except Exception:
continue
rows.append([lem, "noun", lem, pl, g or "", "", "gender:lexicon"])
st["nouns"] += 1
# adjs: form0=positive form1=comparative form2=superlative
for lem in sorted(M._ADJS):
if not lem: continue
try:
cmpr = uw(M.comparative(lem))
sprl = uw(M.superlative(lem))
except Exception:
continue
rows.append([lem, "adj", lem, cmpr, sprl, "", "degree:lexicon"])
st["adjs"] += 1
# verbs (only the ~30 irregular/strong stems the cache carries):
# form0=pres-3sg form1=past-3sg form2=past-participle
if hasattr(M, "_VERBS"):
for lem in sorted({k[0] if isinstance(k, tuple) else k for k in M._VERBS}):
if not lem: continue
try:
f0 = uw(M.finite(lem, "present", "third", "singular"))
f1 = uw(M.finite(lem, "past", "third", "singular"))
pp = uw(M.past_participle(lem))
except Exception:
continue
rows.append([lem, "verb", f0, f1, pp, "", "class:strong/irregular"])
st["verbs"] += 1
return rows, st
def build_la():
M = importlib.import_module("morphology_lat_full")
rows = []; st = {"verbs":0,"nouns":0,"adjs":0}
def dn(lem, c, n):
try:
r = M.decline_noun(lem, c, n)
return uw(r)
except Exception:
return ""
# nouns: dictionary citation — form0=nom-sg form1=gen-sg form2=gender
for lem in sorted(M._NOUNS):
if not lem: continue
nom = dn(lem, "NOM", "SG") or lem
gen = dn(lem, "GEN", "SG")
try: g = uw(M.noun_gender(lem))
except Exception: g = ""
rows.append([lem, "noun", nom, gen, g, "", "case-paradigm nom/gen-sg"])
st["nouns"] += 1
# adjs: three-gender nom-sg citation — form0=masc form1=fem form2=neut
for lem in sorted(M._ADJS):
if not lem: continue
try:
m = uw(M.decline_adj(lem, "NOM", "MASC", "SG")) or lem
f = uw(M.decline_adj(lem, "NOM", "FEM", "SG"))
nt = uw(M.decline_adj(lem, "NOM", "NEUT", "SG"))
except Exception:
continue
rows.append([lem, "adj", m, f, nt, "", "3-gender nom-sg"])
st["adjs"] += 1
# verbs: principal parts — form0=pres-ind-1sg form1=pres-infinitive form2=perf-participle
if hasattr(M, "_VERBS"):
for lem in sorted({k[0] if isinstance(k, tuple) else k for k in M._VERBS}):
if not lem: continue
try:
f0 = uw(M.conjugate(lem, "present", "indicative", "active", "first", "singular"))
inf = uw(M.infinitive(lem, "present", "active"))
pp = uw(M.participle(lem, "perfect", "nom", "m", "singular"))
except Exception:
continue
rows.append([lem, "verb", f0, inf, pp, "", "principal-parts pres1sg/inf/pfppl"])
st["verbs"] += 1
return rows, st
if __name__ == "__main__":
lang = sys.argv[1]; out = sys.argv[2]
rows, st = build_de() if lang == "de" else build_la()
total, _ = write_seed(lang, rows, st, out)
print(f"{lang}: wrote {out} total={total} verbs={st['verbs']} nouns={st['nouns']} adjs={st['adjs']}")
-129
View File
@@ -1,129 +0,0 @@
# -*- coding: utf-8 -*-
"""gen_elp_seed_full.py — emit a FULL-lexicon vocabulary-{lang}.el in the
established ELP seed-fn format (same as vocabulary-non.el / the 18 classical
languages), iterating the ENTIRE morphology_{lang}_full lexicon (every verb,
noun, adjective lemma) — NOT a curated demo core.
Schema per row: [lemma, pos, form0, form1, form2, en_translation, semantic_hint]
Verbs: form0=pres-ind-3sg form1=preterite-3sg form2=past-participle
Nouns: form0=singular form1=plural form2=REAL gender (lexicon)
Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
Output structure (chunked to stay within the proven ~5k-append/function scale):
fn vocab_{lang}_seed_pN(v) -> [[String]] { ... appends ... return v }
fn vocab_{lang}_seed() -> [[String]] { chains all chunks; return v }
fn vocab_{lang}_lookup(w) -> [String] { linear scan }
Usage: python3 gen_elp_seed_full.py <lang> <out.el>
"""
import sys, importlib
CHUNK = 5000
def esc(s):
return str(s).replace("\\", "\\\\").replace('"', '\\"')
def row(fields):
return " let v = native_list_append(v, [" + ", ".join(f'"{esc(f)}"' for f in fields) + "])"
def build_rows(lang, M):
rows = []
stats = {"verbs":0,"nouns":0,"adjs":0}
has = lambda n: hasattr(M, n)
# --- verbs ---
if has("_VERBS") and has("conjugate"):
verbs = sorted({k[0] for k in M._VERBS})
for lem in verbs:
if not lem: continue
try:
f0, s0 = M.conjugate(lem, "ind", "present", "third", "singular")
f1, _ = M.conjugate(lem, "ind", "preterite", "third", "singular")
pp, _ = (M.participle(lem) if has("participle") else ("",""))
except Exception:
continue
vclass = lem[-2:] if lem[-2:] in ("ar","er","ir","re") else lem[-2:]
rows.append([lem, "verb", f0 or "", f1 or "", pp or "", "", "class:"+vclass+" src:"+str(s0)])
stats["verbs"] += 1
# --- nouns ---
if has("_NOUNS") and has("inflect_noun"):
for lem in sorted(M._NOUNS):
if not lem: continue
try:
sg, _ = M.inflect_noun(lem, "singular")
pl, _ = M.inflect_noun(lem, "plural")
g = M.noun_gender(lem) if has("noun_gender") else ""
except Exception:
continue
src = "lexicon" if (isinstance(M._NOUNS.get(lem), dict) and M._NOUNS[lem].get("g")) else "heuristic"
rows.append([lem, "noun", sg or lem, pl or "", g or "", "", "gender:"+src])
stats["nouns"] += 1
# --- adjectives ---
if has("_ADJS") and has("inflect_adj"):
for lem in sorted(M._ADJS):
if not lem: continue
try:
m_sg, _ = M.inflect_adj(lem, "m", "singular")
f_sg, _ = M.inflect_adj(lem, "f", "singular")
m_pl, _ = M.inflect_adj(lem, "m", "plural")
except Exception:
continue
rows.append([lem, "adj", m_sg or lem, f_sg or "", m_pl or "", "", "src:lexicon"])
stats["adjs"] += 1
return rows, stats
def write_seed(lang, rows, stats, out_path):
"""Write vocabulary-{lang}.el in the chunked seed-fn format from prebuilt rows.
Each row is a 7-field list [lemma,pos,f0,f1,f2,gloss,hint]."""
total = len(rows)
chunks = [rows[i:i+CHUNK] for i in range(0, total, CHUNK)] or [[]]
L = []
L.append(f"// vocabulary-{lang}.el — FULL {lang} lexicon for ELP surface realization.")
L.append(f"// Generated by gen_elp_seed_full.py from morphology_{lang}_full")
L.append(f"// (real UniMorph + kaikki.org Wiktionary forms; gender from lexicon, not heuristic).")
L.append(f"// Entries: {total} (verbs={stats['verbs']} nouns={stats['nouns']} adjs={stats['adjs']})")
L.append(f"// Schema: [lemma, pos, form0, form1, form2, en_translation, semantic_hint]")
L.append(f"// verbs: form0=pres-3sg form1=pret-3sg form2=past-participle")
L.append(f"// nouns: form0=sg form1=pl form2=REAL gender adjs: form0=m-sg form1=f-sg form2=m-pl")
L.append("")
for ci, ch in enumerate(chunks):
L.append(f"fn vocab_{lang}_seed_p{ci}(v: [[String]]) -> [[String]] {{")
for r in ch:
L.append(row(r))
L.append(" return v")
L.append("}")
L.append("")
L.append(f"fn vocab_{lang}_seed() -> [[String]] {{")
L.append(" let v: [[String]] = native_list_empty()")
for ci in range(len(chunks)):
L.append(f" let v = vocab_{lang}_seed_p{ci}(v)")
L.append(" return v")
L.append("}")
L.append("")
L.append(f"fn vocab_{lang}_lookup(word: String) -> [String] {{")
L.append(f" let vocab: [[String]] = vocab_{lang}_seed()")
L.append(" let n: Int = native_list_len(vocab)")
L.append(" let i: Int = 0")
L.append(" while i < n {")
L.append(" let entry: [String] = native_list_get(vocab, i)")
L.append(' if str_eq(native_list_get(entry, 0), word) { return entry }')
L.append(" let i = i + 1")
L.append(" }")
L.append(" return native_list_empty()")
L.append("}")
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(L) + "\n")
return total, stats
def emit(lang, out_path):
M = importlib.import_module(f"morphology_{lang}_full")
rows, stats = build_rows(lang, M)
return write_seed(lang, rows, stats, out_path)
if __name__ == "__main__":
lang, out = sys.argv[1], sys.argv[2]
total, stats = emit(lang, out)
print(f"{lang}: wrote {out} total={total} verbs={stats['verbs']} nouns={stats['nouns']} adjs={stats['adjs']}")
-572
View File
@@ -1,572 +0,0 @@
# -*- coding: utf-8 -*-
"""morphology_ca_full.py — production-grade Catalan morphological generator.
Same design as morphology_it_full.py (its Romance sibling); Catalan-specific data.
VERBS
UniMorph Catalan (github.com/unimorph/cat, CC-BY-SA 3.0)
7,535 verb lemmas × paradigm, CLEAN orthography:
present, imperfet (PST;IPFV), pretèrit simple (PST;PFV), futur,
condicional (COND), subjuntiu present (SBJV;PRS) / imperfet (SBJV;PST),
imperatiu (POS;IMP), infinitiu (NFIN), gerundi (V.CVB;PRS),
participi (V.PTCP;PST) — WITH full gender+number agreement forms
(cantat/cantada/cantats/cantades) stored directly.
ca_irreg_verbs.json — verbs UniMorph MISSES or under-populates
(anar, fer, plus core auxiliaries ser/haver/estar/tenir…), extracted from
kaikki.org Catalan by build_ca_irreg.py. Priority layer. Supplies anar,
whose present (vaig/vas/va/anem/aneu/van) is ALSO the PERIPHRASTIC-PRETERITE
auxiliary (vaig cantar = 'I sang') — a hallmark Catalan construction.
NOUNS + ADJECTIVES — kaikki.org Catalan (Wiktionary extract, CC-BY-SA 3.0)
noun lemmas WITH inherent gender + real plural (resolved PER LEMMA).
adjective lemmas with real feminine + plural forms.
Fallbacks degrade, never crash:
verbs : regular -ar/-er/-re/-ir rule generator (+ -car/-gar/-çar spelling).
nouns : gender heuristic + rule pluralization (-a→-es with ç/c/g/j/qu/gu
spelling changes; sibilant-final → -os; else -s). Ambiguous → FLAG.
adjs : -o? no (Catalan masc often consonant/-e); fem -a rule + plural rule.
Confidence flag per form: "lexicon" | "rule" | "fallback" (low → FLAG).
Public API (used by realizer_ca.py):
conjugate(lemma, mood, tense, person, number) -> (form, conf)
peri_pret_aux(person, number) -> form # anar-present, for vaig+INF
participle(lemma, gender, number) -> (form, conf)
gerund(lemma) -> (form, conf)
noun_gender(lemma) -> "m"|"f"
inflect_noun(lemma, number, gender=None) -> (form, conf)
inflect_adj(lemma, gender, number) -> (form, conf)
lexicon_stats() -> dict
"""
import json
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "cat.unimorph")
_IRREG = os.path.join(_HERE, "data", "ca_irreg_verbs.json")
_KAIKKI = os.path.join(_HERE, "data", "kaikki_ca.jsonl")
_CACHE = os.path.join(_HERE, "data", "ca_morph_cache.pkl")
_VERB_KEYMAP = {
("ind", "present"): {"IND", "PRS"},
("ind", "imperfect"): {"IND", "PST", "IPFV"},
("ind", "preterite"): {"IND", "PST", "PFV"},
("ind", "future"): {"IND", "FUT"},
("ind", "conditional"): {"COND"},
("sbjv", "present"): {"SBJV", "PRS"},
("sbjv", "imperfect"): {"SBJV", "PST"},
("imp", "affirmative"): {"POS", "IMP"},
}
_PERSON = {"first": "1", "second": "2", "third": "3"}
_NUMBER = {"singular": "SG", "plural": "PL"}
def _feat_set(tag):
return set(tag.split(";"))
# ── verbs from UniMorph ──────────────────────────────────────────────────────────
def _build_verbs():
verbs = {}
part = {} # lemma -> {("m","SG"):form, ("f","SG"):..., ("m","PL"):..., ("f","PL"):...}
ger = {}
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
f = _feat_set(tag)
head = tag.split(";")[0]
if head == "V.PTCP":
if "PST" in f:
g = "f" if "FEM" in f else "m"
n = "PL" if "PL" in f else "SG"
part.setdefault(lemma, {})[(g, n)] = form
continue
if head == "V.CVB":
if "PRS" in f:
ger.setdefault(lemma, form)
continue
if head != "V":
continue
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person is None or number is None:
continue
for (mood, tense), req in _VERB_KEYMAP.items():
if not req <= f:
continue
if tense == "imperfect" and "PFV" in f:
continue
if tense == "preterite" and "IPFV" in f:
continue
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
break
return verbs, part, ger
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
"diminutive", "augmentative", "pejorative", "comparative",
"superlative", "misspelling", "rare", "informal", "literary",
"poetic", "error-unrecognized-form", "Balearic", "Valencian",
"dated", "nonstandard"}
def _kaikki_gender(arg):
if not arg:
return None
a = str(arg).lower()
if a.startswith("f"):
return "f"
if a.startswith("m"):
return "m"
return None
def _build_nouns_adjs():
nouns = {}
adjs = {}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
pos = d.get("pos")
word = d.get("word", "")
if not word or " " in word:
continue
forms = d.get("forms", []) or []
if pos == "noun":
ht = d.get("head_templates") or []
g = None
if ht:
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
if g is None:
tags = d.get("tags") or []
if "feminine" in tags:
g = "f"
elif "masculine" in tags:
g = "m"
pl = None
for x in forms:
t = set(x.get("tags") or [])
if "plural" in t and not (t & _EXCL_FORM_TAGS):
fm = x.get("form")
if fm and " " not in fm and fm not in ("#", "", "-"):
pl = fm
break
if word not in nouns:
nouns[word] = {"g": g, "SG": word, "PL": pl}
else:
cur = nouns[word]
if cur.get("g") is None and g:
cur["g"] = g
if not cur.get("PL") and pl:
cur["PL"] = pl
elif pos == "adj":
d0 = adjs.setdefault(word, {})
d0.setdefault(("m", "SG"), word)
for x in forms:
t = set(x.get("tags") or [])
fm = x.get("form")
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
continue
if "feminine" in t and "plural" in t:
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
elif "masculine" in t and "plural" in t:
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
elif "feminine" in t:
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
elif "plural" in t:
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
return nouns, adjs
def _build_cache():
verbs, part, ger = _build_verbs()
nouns, adjs = _build_nouns_adjs()
with open(_IRREG, encoding="utf-8") as fh:
irreg = json.load(fh)
data = {"verbs": verbs, "part": part, "ger": ger,
"nouns": nouns, "adjs": adjs, "irreg": irreg}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE):
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
if os.path.getmtime(_CACHE) >= newest:
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
_LEX["irreg"])
_PERI = _IRREGV.get("_peri_pret_aux", {})
# ── regular verb rule fallback ───────────────────────────────────────────────────
def _vclass(lemma):
if lemma.endswith("ar"):
return "ar"
if lemma.endswith("re"):
return "re"
if lemma.endswith("er"):
return "er"
if lemma.endswith("ir"):
return "ir"
return None
# endings [1sg,2sg,3sg,1pl,2pl,3pl] — central Catalan
_REG = {
("ind", "present", "ar"): ["o", "es", "a", "em", "eu", "en"],
("ind", "present", "re"): ["o", "s", "", "em", "eu", "en"],
("ind", "present", "er"): ["o", "s", "", "em", "eu", "en"],
("ind", "present", "ir"): ["o", "es", "", "im", "iu", "en"], # pure -ir (dormir)
("ind", "imperfect", "ar"): ["ava", "aves", "ava", "àvem", "àveu", "aven"],
("ind", "imperfect", "re"): ["ia", "ies", "ia", "íem", "íeu", "ien"],
("ind", "imperfect", "er"): ["ia", "ies", "ia", "íem", "íeu", "ien"],
("ind", "imperfect", "ir"): ["ia", "ies", "ia", "íem", "íeu", "ien"],
("ind", "preterite", "ar"): ["í", "ares", "à", "àrem", "àreu", "aren"],
("ind", "preterite", "re"): ["í", "eres", "é", "érem", "éreu", "eren"],
("ind", "preterite", "er"): ["í", "eres", "é", "érem", "éreu", "eren"],
("ind", "preterite", "ir"): ["í", "ires", "í", "írem", "íreu", "iren"],
("sbjv", "present", "ar"): ["i", "is", "i", "em", "eu", "in"],
("sbjv", "present", "re"): ["i", "is", "i", "em", "eu", "in"],
("sbjv", "present", "er"): ["i", "is", "i", "em", "eu", "in"],
("sbjv", "present", "ir"): ["i", "is", "i", "im", "iu", "in"],
("sbjv", "imperfect", "ar"): ["és", "essis", "és", "éssim", "éssiu", "essin"],
("sbjv", "imperfect", "re"): ["és", "essis", "és", "éssim", "éssiu", "essin"],
("sbjv", "imperfect", "er"): ["és", "essis", "és", "éssim", "éssiu", "essin"],
("sbjv", "imperfect", "ir"): ["ís", "issis", "ís", "íssim", "íssiu", "issin"],
("imp", "affirmative", "ar"): [None, "a", "i", "em", "eu", "in"],
("imp", "affirmative", "re"): [None, "", "i", "em", "eu", "in"],
("imp", "affirmative", "er"): [None, "", "i", "em", "eu", "in"],
("imp", "affirmative", "ir"): [None, "", "i", "im", "iu", "in"],
}
_FUT = ["é", "às", "à", "em", "eu", "an"]
_COND = ["ia", "ies", "ia", "íem", "íeu", "ien"]
def _slot_idx(person, number):
base = {"first": 0, "second": 1, "third": 2}[person]
return base + (0 if number == "singular" else 3)
def _apply_ar_spelling(stem, ending):
"""-car/-gar/-çar/-jar spelling before front (e/i) endings."""
front = ending[:1] in ("e", "i", "é", "í")
if not front:
# ç before back vowel stays; but -çar stem already ends ç
return stem + ending
if stem.endswith("c"):
return stem[:-1] + "qu" + ending
if stem.endswith("g"):
return stem[:-1] + "gu" + ending
if stem.endswith("ç"):
return stem[:-1] + "c" + ending
if stem.endswith("j"):
return stem[:-1] + "g" + ending
if stem.endswith("qu"):
return stem + ending
return stem + ending
def _rule_conjugate(lemma, mood, tense, person, number):
vc = _vclass(lemma)
if vc is None:
return None
body = lemma[:-2]
i = _slot_idx(person, number)
if mood == "ind" and tense in ("future", "conditional"):
# future/cond stem = infinitive (for -re verbs drop final -e)
stem = lemma[:-1] if vc == "re" else lemma
end = (_FUT if tense == "future" else _COND)[i]
return stem + end
table = _REG.get((mood, tense, vc))
if not table:
return None
end = table[i]
if end is None:
return None
if vc == "ar":
return _apply_ar_spelling(body, end)
# -re/-er/-ir: guard double vowel
if body and body[-1:] == end[:1] and end[:1] in "":
return body[:-1] + end
return body + end
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
def conjugate(lemma, mood, tense, person, number):
lemma = lemma.strip().lower()
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number and number[:2].upper()}"
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{_NUMBER.get(number,'?')}"
# UniMorph (cleanly accented) takes priority; the kaikki irregulars layer is a
# FALLBACK for verbs/slots UniMorph lacks (anar, fer, and rarer paradigm cells).
p, n = _PERSON.get(person), _NUMBER.get(number)
if p and n:
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
if form:
return form, "lexicon"
ir = _IRREGV.get(lemma)
if ir and key in ir:
return ir[key], "lexicon"
r = _rule_conjugate(lemma, mood, tense, person, number)
if r is not None:
return r, "rule"
return lemma, "fallback"
def peri_pret_aux(person, number):
"""anar-present auxiliary for the periphrastic preterite (vaig cantar)."""
return _PERI.get(f"{_PERSON.get(person,'3')}|{_NUMBER.get(number,'SG')}", "va")
# ── PUBLIC: participle + gerund ──────────────────────────────────────────────────
def participle(lemma, gender="m", number="singular"):
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
ir = _IRREGV.get(lemma)
base = None
if ir and "part" in ir:
# prefer explicit irregular agreement form (part_mSG/part_fSG/...)
exact = ir.get("part_" + g + num)
if exact:
return exact, "lexicon"
base = ir["part"]
elif lemma in _PART:
table = _PART[lemma]
if (g, num) in table:
return table[(g, num)], "lexicon"
base = table.get(("m", "SG"))
if base is None:
vc = _vclass(lemma)
if vc == "ar":
base = lemma[:-2] + "at"
elif vc == "ir":
base = lemma[:-2] + "it"
elif vc in ("er", "re"):
base = lemma[:-2] + "ut"
else:
return lemma, "fallback"
conf = "rule"
else:
conf = "lexicon"
# agreement on -t/-ut/-at/-it participles: m.sg base, f.sg +a (-da? no: -ada),
# Catalan: cantat/cantada/cantats/cantades; -t → f -da, pl -ts/-des
if base.endswith("t"):
stem = base[:-1]
forms = {"m|SG": base, "f|SG": stem + "da",
"m|PL": base + "s", "f|PL": stem + "des"}
return forms[f"{g}|{num}"], conf
if base.endswith("s"): # after sibilant participle (rare): pres->presa
stem = base
forms = {"m|SG": base, "f|SG": base + "a",
"m|PL": base + "os", "f|PL": base + "es"}
return forms[f"{g}|{num}"], conf
return base, conf
def gerund(lemma):
lemma = lemma.strip().lower()
ir = _IRREGV.get(lemma)
if ir and "ger" in ir:
return ir["ger"], "lexicon"
if lemma in _GER:
return _GER[lemma], "lexicon"
vc = _vclass(lemma)
if vc == "ar":
return lemma[:-2] + "ant", "rule"
if vc in ("er", "re"):
return lemma[:-2] + "ent", "rule"
if vc == "ir":
return lemma[:-2] + "int", "rule"
return lemma, "fallback"
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
_FEM_SUF = ("ció", "sió", "tat", "tud", "esa", "esa", "dat", "ança", "ència",
"ància", "tud", "ícia", "esa", "or") # note -or is mixed; kaikki wins
_MASC_SUF = ("atge", "ment", " isme", "or")
def _gender_heuristic(noun):
for suf in ("ció", "sió", "tat", "tud", "esa", "ança", "ència", "ància",
"ícia", "etat"):
if noun.endswith(suf):
return "f"
if noun.endswith("a") and not noun.endswith("ma"):
return "f"
return "m"
def noun_gender(lemma):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if d and d.get("g") in ("m", "f"):
return d["g"]
return _gender_heuristic(lemma)
def _rule_plural(noun, gender):
"""Deterministic Catalan pluralization. (form, ok); ok=False FLAGS ambiguity."""
if not noun:
return noun, True
# stressed final vowel with accent → +ns (mà→mans is irregular; but capità→capitans)
if noun[-1:] in ("à", "é", "í", "ó", "ú"):
return noun + "ns", True
if noun.endswith("ça"):
return noun[:-2] + "ces", True # plaça→places
if noun.endswith("ca"):
return noun[:-2] + "ques", True # branca→branques
if noun.endswith("ga"):
return noun[:-2] + "gues", True # amiga→amigues
if noun.endswith("ja"):
return noun[:-2] + "ges", True # pluja→pluges
if noun.endswith("qua"):
return noun[:-3] + "qües", True
if noun.endswith("gua"):
return noun[:-3] + "gües", True
if noun.endswith("a"):
return noun[:-1] + "es", True # casa→cases
# sibilant-final → -os
if noun.endswith(("s", "ç", "x", "ig")) or noun.endswith(("ix", "tx", "tj")):
if noun.endswith("ç"):
return noun[:-1] + "ços", True # braç→braços
return noun + "os", True # peix→peixos, gas→gasos
if noun[-1:] in ("e", "i", "o", "u"):
return noun + "s", True
# consonant-final
return noun + "s", True
def inflect_noun(lemma, number, gender=None):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if number == "singular":
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
if d and d.get("PL"):
return d["PL"], "lexicon"
g = gender or noun_gender(lemma)
form, ok = _rule_plural(lemma, g)
return form, ("rule" if ok else "fallback")
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
def _fem_of(adj):
"""Regular Catalan feminine: consonant/-o? Catalan masc usually consonant or -e.
default +a with spelling changes; -e→-a for some; but many are invariable."""
a = adj
if a.endswith("a"):
return a
if a.endswith("e"):
return a[:-1] + "a" # ample→? actually 'ample' invariable; kaikki wins
if a.endswith("u"):
return a + "a"
if a.endswith("c"):
return a[:-1] + "ca" # ric→rica
if a.endswith("t"):
return a + "a" # alt→alta
return a + "a"
def inflect_adj(lemma, gender, number):
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
d = _ADJS.get(lemma)
if d:
form = d.get((g, num))
if form:
return form, "lexicon"
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
if num == "PL":
pl, ok = _rule_plural(sg, g)
return pl, ("rule" if ok else "fallback")
return sg, "lexicon"
# rule fallback
base = lemma if g == "m" else _fem_of(lemma)
if num == "SG":
return base, "rule"
pl, ok = _rule_plural(base, g)
return pl, ("rule" if ok else "fallback")
def lexicon_stats():
return {
"verb_source": "UniMorph Catalan (github.com/unimorph/cat) + kaikki.org "
"irregulars (anar/fer/auxiliaries)",
"noun_adj_source": "kaikki.org Catalan (Wiktionary extract)",
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
"unimorph_verb_forms": len(_VERBS),
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
"irregular_verb_lemmas": len([k for k in _IRREGV if not k.startswith("_")]),
"participle_lemmas": len(_PART),
"gerund_lemmas": len(_GER),
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
}
if __name__ == "__main__":
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
tests = [
("cantar", "ind", "present", "first", "singular", "canto"),
("cantar", "ind", "present", "third", "plural", "canten"),
("ser", "ind", "present", "third", "singular", "és"),
("haver", "ind", "present", "first", "singular", "he"),
("anar", "ind", "present", "first", "singular", "vaig"),
("fer", "ind", "present", "third", "singular", "fa"),
("perdre", "ind", "present", "first", "singular", "perdo"),
("dormir", "ind", "present", "third", "plural", "dormen"),
("cantar", "ind", "future", "first", "singular", "cantaré"),
("cantar", "ind", "preterite", "third", "singular", "cantà"),
("tenir", "sbjv", "present", "first", "singular", "tingui"),
]
ok = 0
for lemma, mood, tense, per, num, exp in tests:
got, conf = conjugate(lemma, mood, tense, per, num)
flag = "OK " if got == exp else "XX "
ok += got == exp
print(f" {flag}{lemma:8} {mood}/{tense:11} {per[:3]}.{num[:2]} -> {got:10} ({conf}) exp={exp}")
print(f"verb tests {ok}/{len(tests)}")
print(" peri-pret anar: 1sg=", peri_pret_aux("first", "singular"),
"3pl=", peri_pret_aux("third", "plural"))
print(" gender casa=", noun_gender("casa"), "home=", noun_gender("home"),
"cavall=", noun_gender("cavall"), "cançó=", noun_gender("cançó"))
print(" plural casa->", inflect_noun("casa", "plural"),
"| plaça->", inflect_noun("plaça", "plural"),
"| peix->", inflect_noun("peix", "plural"),
"| braç->", inflect_noun("braç", "plural"),
"| home->", inflect_noun("home", "plural"))
print(" adj: alt/f/sg->", inflect_adj("alt", "f", "singular"),
"| bonic/f/pl->", inflect_adj("bonic", "f", "plural"),
"| vermell/f/sg->", inflect_adj("vermell", "f", "singular"))
print(" part: cantar/f/sg->", participle("cantar", "f", "singular"),
"| veure/f/pl->", participle("veure", "f", "plural"),
"| fer/m/sg->", participle("fer", "m", "singular"))
print(" ger: fer->", gerund("fer"), "| cantar->", gerund("cantar"))
-423
View File
@@ -1,423 +0,0 @@
# -*- coding: utf-8 -*-
"""morphology_de_full.py — production German morphological generator.
Real data, no toy tables:
PRIMARY — UniMorph German (github.com/unimorph/deu, CC-BY-SA 3.0).
~219k noun forms, ~199k verb forms. Supplies:
nouns : gender (MASC/FEM/NEUT) + case×number paradigm
(N;NOM/ACC/DAT/GEN; MASC/FEM/NEUT; SG/PL) — the genitive -(e)s,
dative-plural -n and the five plural classes are REAL forms, not
guessed.
verbs : full finite paradigm IND;{SG,PL};{1,2,3};{PRS,PST}, the past
participle (V.PTCP;PST, incl. reattached separable prefix
'zugefügt'), and — crucially for V2 — the SEPARATED finite form
UniMorph records directly ('füge zu', 'steht auf').
adjs : comparative / superlative (ADJ;CMPR, ADJ;SPRL).
SECONDARY — kaikki.org German (Wiktionary, CC-BY-SA/GFDL). Gap-fills noun
gender + plural where UniMorph is thin. Never overrides UniMorph.
Rule fallbacks (flagged 'rule'/'fallback') for lemmas absent from both lexicons:
present : -e/-st/-t/-en/-t/-en with e-epenthesis after -t/-d/-chn stems
plural : gender heuristic (fem -> -(e)n, else -e / umlaut left to lexicon)
ppart : weak ge-…-t
Adjective ENDINGS are rule-computed by the realizer (regular closed table);
this module only supplies the comparative/superlative STEM.
Perfect auxiliary (haben vs sein): sein for a curated set of intransitive
motion / change-of-state verbs (real German lexical property), else haben.
Public API:
noun_gender(lemma) -> 'm'|'f'|'n'
decline_noun(lemma, case, number) -> (form, conf)
pluralize(lemma) -> (form, conf)
finite(lemma, tense, person, number) -> (form, conf) # may contain ' prefix'
nonfinite(lemma, req) -> (form, conf) # req: 'inf'|'ppart'
past_participle(lemma) -> (form, conf)
separable_prefix(lemma) -> str|None
perfect_aux(lemma) -> 'haben'|'sein'
comparative(lemma)/superlative(lemma) -> (stem, conf)
lexicon_stats() -> dict
"""
import json
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "deu.unimorph")
_KAIKKI = os.path.join(_HERE, "data", "kaikki_de.jsonl")
_CACHE = os.path.join(_HERE, "data", "de_morph_cache.pkl")
_GENDER = {"MASC": "m", "FEM": "f", "NEUT": "n"}
# intransitive motion / change-of-state verbs that take SEIN in the perfect
_SEIN = {"gehen", "kommen", "fahren", "laufen", "rennen", "reisen", "fallen",
"steigen", "sinken", "wachsen", "sterben", "geschehen", "passieren",
"werden", "bleiben", "sein", "aufstehen", "einschlafen", "aufwachen",
"ankommen", "abfahren", "aufsteigen", "erscheinen", "verschwinden",
"fliegen", "schwimmen", "springen", "begegnen", "folgen", "gelingen",
"wandern", "ziehen", "flüchten", "eintreten", "einsteigen", "aussteigen"}
# hardcoded high-frequency irregular / auxiliary / modal paradigms (closed class,
# verified) — consulted before the lexicon so aux+modal chains are always correct.
_CORE = {
"sein": {"prs": {("first", "singular"): "bin", ("second", "singular"): "bist",
("third", "singular"): "ist", ("first", "plural"): "sind",
("second", "plural"): "seid", ("third", "plural"): "sind"},
"pst": {("first", "singular"): "war", ("second", "singular"): "warst",
("third", "singular"): "war", ("first", "plural"): "waren",
("second", "plural"): "wart", ("third", "plural"): "waren"},
"ppart": "gewesen"},
"haben": {"prs": {("first", "singular"): "habe", ("second", "singular"): "hast",
("third", "singular"): "hat", ("first", "plural"): "haben",
("second", "plural"): "habt", ("third", "plural"): "haben"},
"pst": {("first", "singular"): "hatte", ("second", "singular"): "hattest",
("third", "singular"): "hatte", ("first", "plural"): "hatten",
("second", "plural"): "hattet", ("third", "plural"): "hatten"},
"ppart": "gehabt"},
"werden": {"prs": {("first", "singular"): "werde", ("second", "singular"): "wirst",
("third", "singular"): "wird", ("first", "plural"): "werden",
("second", "plural"): "werdet", ("third", "plural"): "werden"},
"pst": {("first", "singular"): "wurde", ("second", "singular"): "wurdest",
("third", "singular"): "wurde", ("first", "plural"): "wurden",
("second", "plural"): "wurdet", ("third", "plural"): "wurden"},
"ppart": "geworden"},
}
_MODAL_PRS = {
"können": ("kann", "kannst", "kann", "können", "könnt", "können"),
"müssen": ("muss", "musst", "muss", "müssen", "müsst", "müssen"),
"wollen": ("will", "willst", "will", "wollen", "wollt", "wollen"),
"sollen": ("soll", "sollst", "soll", "sollen", "sollt", "sollen"),
"dürfen": ("darf", "darfst", "darf", "dürfen", "dürft", "dürfen"),
"mögen": ("mag", "magst", "mag", "mögen", "mögt", "mögen"),
}
_MODAL_PST = {
"können": ("konnte", "konntest", "konnte", "konnten", "konntet", "konnten"),
"müssen": ("musste", "musstest", "musste", "mussten", "musstet", "mussten"),
"wollen": ("wollte", "wolltest", "wollte", "wollten", "wolltet", "wollten"),
"sollen": ("sollte", "solltest", "sollte", "sollten", "solltet", "sollten"),
"dürfen": ("durfte", "durftest", "durfte", "durften", "durftet", "durften"),
"mögen": ("mochte", "mochtest", "mochte", "mochten", "mochtet", "mochten"),
}
_PN_ORDER = [("first", "singular"), ("second", "singular"), ("third", "singular"),
("first", "plural"), ("second", "plural"), ("third", "plural")]
_MODAL_PPART = {"können": "gekonnt", "müssen": "gemusst", "wollen": "gewollt",
"sollen": "gesollt", "dürfen": "gedurft", "mögen": "gemocht"}
for _m, _forms in _MODAL_PRS.items():
_CORE[_m] = {"prs": dict(zip(_PN_ORDER, _forms)),
"pst": dict(zip(_PN_ORDER, _MODAL_PST[_m])),
"ppart": _MODAL_PPART[_m]}
def _person_num(tags):
p = n = None
for t in tags:
if t in ("1", "2", "3"):
p = {"1": "first", "2": "second", "3": "third"}[t]
elif t == "SG":
n = "singular"
elif t == "PL":
n = "plural"
return p, n
def _build_from_unimorph():
nouns, verbs, adjs = {}, {}, {}
if not os.path.exists(_UNIMORPH):
return nouns, verbs, adjs
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tagstr = parts
tags = tagstr.split(";")
head = tags[0]
tset = set(tags)
if head == "N":
rec = nouns.setdefault(lemma, {"g": None, "cases": {}, "pl": None})
g = next((_GENDER[t] for t in tags if t in _GENDER), None)
if g and not rec["g"]:
rec["g"] = g
case = next((t for t in tags if t in ("NOM", "ACC", "DAT", "GEN")), None)
num = "plural" if "PL" in tset else ("singular" if "SG" in tset else None)
if case and num:
rec["cases"].setdefault((case, num), form)
if case == "NOM" and num == "plural" and not rec["pl"]:
rec["pl"] = form
elif head.startswith("V"):
rec = verbs.setdefault(lemma, {"prs": {}, "pst": {}, "ppart": None})
if "PTCP" in head and "PST" in tset:
rec["ppart"] = rec["ppart"] or form
elif "IND" in tset and ("PRS" in tset or "PST" in tset):
p, n = _person_num(tags)
if p and n:
slot = "prs" if "PRS" in tset else "pst"
rec[slot].setdefault((p, n), form)
elif head == "ADJ":
rec = adjs.setdefault(lemma, {})
if "CMPR" in tset:
rec.setdefault("cmpr", form.replace("am ", "").strip())
elif "SPRL" in tset:
rec.setdefault("sprl", form.replace("am ", "").replace("sten", "st")
if form.endswith("sten") else form.replace("am ", ""))
return nouns, verbs, adjs
def _build_from_kaikki(nouns):
"""Gap-fill noun gender + plural from kaikki German."""
if not os.path.exists(_KAIKKI):
return
_g = {"masculine": "m", "feminine": "f", "neuter": "n", "m": "m", "f": "f", "n": "n"}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
if d.get("pos") != "noun":
continue
w = d.get("word", "")
if not w or not w[0].isalpha() or " " in w:
continue
rec = nouns.setdefault(w, {"g": None, "cases": {}, "pl": None})
# GENDER: Wiktionary gender is hand-curated and OVERRIDES UniMorph's
# auto-tagged gender, which has known errors (e.g. UniMorph deu mis-
# records Zeit=MASC, Wagen=NEUT; Wiktionary has f, m correctly).
for h in d.get("head_templates", []) or []:
a = h.get("args", {}) or {}
raw = a.get("1") or a.get("g") or ""
code = str(raw).split(",")[0].strip().lower()
if code in _g:
rec["g"] = _g[code]
break
if not rec["pl"]:
for f in d.get("forms", []) or []:
t = set(f.get("tags", []) or [])
if "plural" in t and f.get("form") and "genitive" not in t:
rec["pl"] = f["form"]
break
def _build_cache():
nouns, verbs, adjs = _build_from_unimorph()
_build_from_kaikki(nouns)
data = {"nouns": nouns, "verbs": verbs, "adjs": adjs}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE):
srcs = [p for p in (_UNIMORPH, _KAIKKI) if os.path.exists(p)]
newest = max((os.path.getmtime(p) for p in srcs), default=0)
if os.path.getmtime(_CACHE) >= newest:
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_NOUNS, _VERBS, _ADJS = _LEX["nouns"], _LEX["verbs"], _LEX["adjs"]
# ── nouns ────────────────────────────────────────────────────────────────────────
def noun_gender(lemma):
rec = _NOUNS.get(lemma) or _NOUNS.get(lemma.capitalize())
if rec and rec.get("g"):
return rec["g"]
# last-resort rule: -ung/-heit/-keit/-schaft/-tät/-ion -> f ; -chen/-lein -> n
low = lemma.lower()
if low.endswith(("ung", "heit", "keit", "schaft", "tät", "ion", "ik", "ei")):
return "f"
if low.endswith(("chen", "lein", "ment", "um")):
return "n"
return "m"
def pluralize(lemma):
rec = _NOUNS.get(lemma) or _NOUNS.get(lemma.capitalize())
if rec and rec.get("pl"):
return rec["pl"], "lexicon"
g = noun_gender(lemma)
if g == "f":
return (lemma + "en" if not lemma.endswith("e") else lemma + "n"), "rule"
return (lemma if lemma.endswith(("er", "en", "el")) else lemma + "e"), "rule"
def decline_noun(lemma, case, number):
"""case in NOM/ACC/DAT/GEN, number in singular/plural."""
rec = _NOUNS.get(lemma) or _NOUNS.get(lemma.capitalize())
if case == "DAT" and number == "singular":
# modern German drops the archaic dative -e ('dem Kinde' -> 'dem Kind');
# the article carries the case. Keep bare nominative form.
base = (rec or {}).get("cases", {}).get(("NOM", "singular")) or lemma
return base, ("lexicon" if rec else "rule")
if rec and rec.get("cases", {}).get((case, number)):
return rec["cases"][(case, number)], "lexicon"
if number == "plural":
pl, c = pluralize(lemma)
if case == "DAT" and not pl.endswith("n") and not pl.endswith("s"):
return pl + "n", c # dative plural -n
return pl, c
# singular
g = noun_gender(lemma)
if case == "GEN" and g in ("m", "n"):
return (lemma + "es" if lemma.endswith(("s", "ß", "z", "x")) else lemma + "s"), "rule"
return lemma, "lexicon" if rec else "rule"
# ── verbs ──────────────────────────────────────────────────────────────────────--
_PRS_ENDINGS = {("first", "singular"): "e", ("second", "singular"): "st",
("third", "singular"): "t", ("first", "plural"): "en",
("second", "plural"): "t", ("third", "plural"): "en"}
def _stem(lemma):
if lemma.endswith("en"):
return lemma[:-2]
if lemma.endswith("n"):
return lemma[:-1]
return lemma
def separable_prefix(lemma):
"""Return the separable prefix if the lemma is a separable-prefix verb."""
rec = _VERBS.get(lemma)
if rec:
for (_p, _n), form in rec.get("prs", {}).items():
if " " in form:
return form.rsplit(" ", 1)[1]
_SEP = ("auf", "aus", "ab", "an", "ein", "mit", "nach", "vor", "zu", "zurück",
"weg", "hin", "her", "los", "bei", "fest", "fort", "um", "zusammen")
_INSEP = ("be", "ge", "er", "ver", "zer", "ent", "emp", "miss")
for p in sorted(_SEP, key=len, reverse=True):
if lemma.startswith(p) and len(lemma) > len(p) + 2 \
and not lemma.startswith(_INSEP):
return p
return None
def finite(lemma, tense, person, number):
"""Present/past finite. For separable verbs the returned string is the
UniMorph SEPARATED form 'stem prefix' (realizer places prefix per V2)."""
slot = "prs" if tense == "present" else "pst"
if lemma in _CORE and _CORE[lemma].get(slot, {}).get((person, number)):
return _CORE[lemma][slot][(person, number)], "lexicon"
rec = _VERBS.get(lemma)
if rec and rec.get(slot, {}).get((person, number)):
return rec[slot][(person, number)], "lexicon"
# rule fallback (present only reliable; past weak -te)
stem = _stem(lemma)
pref = separable_prefix(lemma)
if pref:
stem = _stem(lemma[len(pref):])
if tense == "present":
end = _PRS_ENDINGS[(person, number)]
if stem.endswith(("t", "d", "chn", "ffn", "gn")) and end in ("st", "t"):
end = "e" + end
form = stem + end
else:
form = stem + ("ete" if stem.endswith(("t", "d")) else "te")
if (person, number) == ("second", "singular"):
form += "st"
elif number == "plural" and person != "second":
form += "n"
elif (person, number) == ("second", "plural"):
form += "t"
if pref:
return f"{form} {pref}", "rule"
return form, "rule"
def _weak_t(stem):
return stem + ("et" if stem.endswith(("t", "d", "chn", "ffn", "gn")) else "t")
def past_participle(lemma):
if lemma in _CORE:
return _CORE[lemma]["ppart"], "lexicon"
rec = _VERBS.get(lemma)
if rec and rec.get("ppart"):
return rec["ppart"], "lexicon"
stem = _stem(lemma)
pref = separable_prefix(lemma)
_INSEP = ("be", "ge", "er", "ver", "zer", "ent", "emp", "miss")
if pref:
inner = _stem(lemma[len(pref):])
return pref + "ge" + _weak_t(inner), "rule"
if lemma.startswith(_INSEP):
return _weak_t(stem), "rule"
return "ge" + _weak_t(stem), "rule"
def nonfinite(lemma, req):
if req == "ppart":
return past_participle(lemma)
return lemma, "lexicon" if lemma in _VERBS else "rule" # infinitive
def perfect_aux(lemma):
return "sein" if lemma in _SEIN else "haben"
# ── adjectives ────────────────────────────────────────────────────────────────---
_ADJ_IRREG_SPRL = {"gut": "best", "groß": "größt", "hoch": "höchst",
"nah": "nächst", "viel": "meist", "gern": "liebst"}
def comparative(lemma):
rec = _ADJS.get(lemma)
if rec and rec.get("cmpr"):
return rec["cmpr"], "lexicon"
return lemma + "er", "rule"
def superlative(lemma):
"""Return the bare superlative STEM (realizer adds 'am ...en' or '-e' ending)."""
if lemma in _ADJ_IRREG_SPRL:
return _ADJ_IRREG_SPRL[lemma], "lexicon"
# derive from the comparative so umlaut is carried (alt->älter->ältest)
cmpr, cconf = comparative(lemma)
base = cmpr[:-2] if cmpr.endswith("er") else lemma
end = "est" if base.endswith(("t", "d", "s", "ß", "z", "sch")) else "st"
return base + end, cconf
def lexicon_stats():
return {
"source": "UniMorph deu (primary) + kaikki.org German (gap-fill gender/plural)",
"license": "CC-BY-SA 3.0 (UniMorph); CC-BY-SA/GFDL (Wiktionary)",
"noun_lemmas": len(_NOUNS),
"nouns_with_gender": sum(1 for v in _NOUNS.values() if v.get("g")),
"nouns_with_plural": sum(1 for v in _NOUNS.values() if v.get("pl")),
"verb_lemmas": len(_VERBS),
"verbs_with_ppart": sum(1 for v in _VERBS.values() if v.get("ppart")),
"adj_lemmas": len(_ADJS),
}
if __name__ == "__main__":
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
for w in ("Hund", "Frau", "Kind", "Mann", "Buch", "Blume"):
print(f" {w}: gender={noun_gender(w)} pl={pluralize(w)} "
f"gen.sg={decline_noun(w, 'GEN', 'singular')} "
f"dat.pl={decline_noun(w, 'DAT', 'plural')}")
for v in ("machen", "gehen", "aufstehen", "sein", "haben", "arbeiten"):
print(f" {v}: 3sg.prs={finite(v, 'present', 'third', 'singular')} "
f"3sg.pst={finite(v, 'past', 'third', 'singular')} "
f"ppart={past_participle(v)} aux={perfect_aux(v)} sep={separable_prefix(v)}")
for a in ("schnell", "gut", "groß", "alt"):
print(f" {a}: cmpr={comparative(a)} sprl={superlative(a)}")
-562
View File
@@ -1,562 +0,0 @@
"""morphology_es_full.py — production-grade Spanish morphological generator.
NOT a toy. Backed by a real, broad, licensed lexicon:
UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0, Wiktionary-derived)
1,196,245 inflected forms:
6,695 verb lemmas — full paradigms: indicative (present/preterite/
imperfect/future), conditional, present & imperfect
subjunctive, affirmative imperative, formal/informal
48,353 noun lemmas — WITH inherent gender (N;FEM/MASC;SG/PL)
16,984 adj lemmas — gender + number paradigms
Fallbacks (so we degrade, never crash, on out-of-vocabulary input):
- verbs : mlconjug3 (ML paradigm model, conjugates ANY Spanish verb) then a
hand-rolled regular-ending generator
- nouns : gender heuristic (endings) + regular pluralization
- adjs : -o/-a gender rule + regular pluralization
Every generated form carries a CONFIDENCE flag:
"lexicon" form came straight from UniMorph (trust: high)
"model" form came from mlconjug3 (trust: high)
"rule" form came from a deterministic rule (trust: medium)
"fallback" we could not inflect; returned lemma as-is (trust: low → FLAG)
Public API (used by realizer_es.py):
conjugate(lemma, mood, tense, person, number, formality="informal") -> (form, conf)
participle(lemma) -> (form, conf) # past participle (compound tenses)
gerund(lemma) -> (form, conf)
noun_gender(lemma) -> "m"|"f"
inflect_noun(lemma, number) -> (form, conf)
inflect_adj(lemma, gender, number) -> (form, conf)
attach_enclitics(verb_form, clitics) -> str # accent-correct enclisis
lexicon_stats() -> dict
"""
import os
import pickle
import unicodedata
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "spa.unimorph")
_CACHE = os.path.join(_HERE, "data", "es_morph_cache.pkl")
# ── canonical feature keys the realizer speaks, mapped to UniMorph tags ─────────
# mood/tense pair -> the UniMorph feature substring that identifies it
_VERB_KEYMAP = {
("ind", "present"): ("IND", "PRS", None),
("ind", "preterite"): ("IND", "PST", "PFV"),
("ind", "imperfect"): ("IND", "PST", "IPFV"),
("ind", "future"): ("IND", "FUT", None),
("ind", "conditional"):("COND", None, None),
("sbjv", "present"): ("SBJV", "PRS", None),
("sbjv", "imperfect"): ("SBJV", "PST", "LGSPEC1"), # -ra form
("imp", "present"): ("POS", "IMP", None),
}
_PERSON = {"first": "1", "second": "2", "third": "3"}
_NUMBER = {"singular": "SG", "plural": "PL"}
# ── build / load the compact lexicon ───────────────────────────────────────────
def _feat_set(tag):
return set(tag.split(";"))
def _build_cache():
verbs = {} # (lemma, canonkey) -> form canonkey e.g. "ind|present|1|SG|infm"
nouns = {} # lemma -> {"g": "m"/"f", "SG": form, "PL": form}
adjs = {} # lemma -> {("m","SG"): form, ...}
part = {} # lemma -> masc-sg participle
ger = {} # lemma -> gerund
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
f = _feat_set(tag)
head = tag.split(";")[0]
if head == "V":
# skip clitic-bearing rows (we generate clitics ourselves)
if "PRO" in f:
continue
if "V.PTCP" in f and "PST" in f and "MASC" in f and "SG" in f:
part.setdefault(lemma, form)
continue
if "V.CVB" in f or "NFIN" in f or "V.PTCP" in f:
if "V.CVB" in f:
ger.setdefault(lemma, form)
continue
# identify mood/tense
mt = None
for (mood, tense), (a, b, c) in _VERB_KEYMAP.items():
if a not in f:
continue
if b is not None and b not in f:
continue
if c is not None and c not in f:
continue
# disambiguate IND;PST needing PFV vs IPFV
if a == "IND" and b == "PST" and c not in f:
continue
mt = (mood, tense)
break
if mt is None:
continue
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person is None or number is None:
continue
formal = "form" if "FORM" in f else ("infm" if "INFM" in f else "any")
key = f"{mt[0]}|{mt[1]}|{person}|{number}|{formal}"
verbs.setdefault((lemma, key), form)
elif head == "N":
# substring test handles epicene "MASC+FEM" (-> masc citation)
g = "m" if "MASC" in tag else ("f" if "FEM" in tag else None)
num = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if num is None:
continue
# store forms keyed by (gender,number); animate nouns list BOTH
# genders under one lemma (niño -> niño/niña). Resolve citation
# gender in a post-pass (gender of the row whose form == lemma).
d = nouns.setdefault(lemma, {})
d.setdefault("_rows", []).append((g, num, form))
elif head == "ADJ":
g = "m" if "MASC" in tag else ("f" if "FEM" in tag else "m")
num = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if num is None:
continue
adjs.setdefault(lemma, {})[(g, num)] = form
# post-pass: resolve noun citation gender + default SG/PL forms
for lemma, d in nouns.items():
rows = d.pop("_rows", [])
# citation gender = gender of the row whose form == lemma; else first MASC;
# else first seen gender.
cite_g = None
for g, num, form in rows:
if form == lemma and g:
cite_g = g
break
if cite_g is None:
for g, num, form in rows:
if g == "m":
cite_g = "m"
break
if cite_g is None:
cite_g = next((g for g, _, _ in rows if g), "m")
d["g"] = cite_g
for g, num, form in rows:
d[(g, num)] = form
d["SG"] = d.get((cite_g, "SG")) or next((f for g, n, f in rows if n == "SG"), lemma)
d["PL"] = d.get((cite_g, "PL")) or next((f for g, n, f in rows if n == "PL"), None)
# post-pass: UniMorph omits the identity inflection (masc-sg == lemma) for
# adjectives, so fill it in; without this a fem-sg row wrongly satisfies a
# masc-sg request (alto -> alta bug).
for lemma, d in adjs.items():
d.setdefault(("m", "SG"), lemma)
data = {"verbs": verbs, "nouns": nouns, "adjs": adjs, "part": part, "ger": ger}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE) and os.path.getmtime(_CACHE) >= os.path.getmtime(_UNIMORPH):
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_VERBS, _NOUNS, _ADJS, _PART, _GER = (
_LEX["verbs"], _LEX["nouns"], _LEX["adjs"], _LEX["part"], _LEX["ger"])
# ── mlconjug3 fallback (lazy) ───────────────────────────────────────────────────
_MLC = None
_MLC_TENSE = { # (mood,tense) -> (mlconjug mood label, tense label)
("ind", "present"): ("Indicativo", "Indicativo presente"),
("ind", "preterite"): ("Indicativo", "Indicativo pretérito perfecto simple"),
("ind", "imperfect"): ("Indicativo", "Indicativo pretérito imperfecto"),
("ind", "future"): ("Indicativo", "Indicativo futuro"),
("ind", "conditional"): ("Condicional", "Condicional Condicional"),
("sbjv", "present"): ("Subjuntivo", "Subjuntivo presente"),
("sbjv", "imperfect"): ("Subjuntivo", "Subjuntivo pretérito imperfecto 1"),
("imp", "present"): ("Imperativo", "Imperativo Afirmativo"),
}
_MLC_SLOT = { # (person,number) -> mlconjug slot key
("first", "singular"): "1s", ("second", "singular"): "2s",
("third", "singular"): "3s", ("first", "plural"): "1p",
("second", "plural"): "2p", ("third", "plural"): "3p",
}
def _mlc_conjugate(lemma, mood, tense, person, number):
global _MLC
try:
if _MLC is None:
from mlconjug3 import Conjugator
_MLC = Conjugator(language="es")
v = _MLC.conjugate(lemma)
if v is None:
return None
info = v.conjug_info
m, t = _MLC_TENSE.get((mood, tense), (None, None))
if m is None or m not in info or t not in info[m]:
return None
block = info[m][t]
slot = _MLC_SLOT.get((person, number))
if isinstance(block, dict) and slot in block and block[slot]:
return block[slot]
return None
except Exception:
return None
# ── regular-ending rule fallback (last resort, deterministic) ───────────────────
def _vclass(lemma):
return lemma[-2:] if lemma[-2:] in ("ar", "er", "ir") else "ar"
def _stem(lemma):
return lemma[:-2]
_REG = {
("ind", "present", "ar"): ["o", "as", "a", "amos", "áis", "an"],
("ind", "present", "er"): ["o", "es", "e", "emos", "éis", "en"],
("ind", "present", "ir"): ["o", "es", "e", "imos", "ís", "en"],
("ind", "preterite", "ar"): ["é", "aste", "ó", "amos", "asteis", "aron"],
("ind", "preterite", "er"): ["í", "iste", "", "imos", "isteis", "ieron"],
("ind", "preterite", "ir"): ["í", "iste", "", "imos", "isteis", "ieron"],
("ind", "imperfect", "ar"): ["aba", "abas", "aba", "ábamos", "abais", "aban"],
("ind", "imperfect", "er"): ["ía", "ías", "ía", "íamos", "íais", "ían"],
("ind", "imperfect", "ir"): ["ía", "ías", "ía", "íamos", "íais", "ían"],
("sbjv", "present", "ar"): ["e", "es", "e", "emos", "éis", "en"],
("sbjv", "present", "er"): ["a", "as", "a", "amos", "áis", "an"],
("sbjv", "present", "ir"): ["a", "as", "a", "amos", "áis", "an"],
("sbjv", "imperfect", "ar"): ["ara", "aras", "ara", "áramos", "arais", "aran"],
("sbjv", "imperfect", "er"): ["iera", "ieras", "iera", "iéramos", "ierais", "ieran"],
("sbjv", "imperfect", "ir"): ["iera", "ieras", "iera", "iéramos", "ierais", "ieran"],
}
_FUT = ["é", "ás", "á", "emos", "éis", "án"]
_COND = ["ía", "ías", "ía", "íamos", "íais", "ían"]
def _slot_idx(person, number):
base = {"first": 0, "second": 1, "third": 2}[person]
return base + (0 if number == "singular" else 3)
def _rule_conjugate(lemma, mood, tense, person, number):
if len(lemma) < 3 or lemma[-2:] not in ("ar", "er", "ir"):
return None
vc, st, i = _vclass(lemma), _stem(lemma), _slot_idx(person, number)
if tense == "future":
return lemma + _FUT[i]
if tense == "conditional":
return lemma + _COND[i]
table = _REG.get((mood, tense, vc))
if table:
return st + table[i]
if mood == "imp" and tense == "present":
# affirmative tú imperative = 3sg present indicative
pres = _REG.get(("ind", "present", vc))
return st + pres[2] if number == "singular" else st + pres[5]
return None
# ── PUBLIC: verb conjugation ────────────────────────────────────────────────────
def conjugate(lemma, mood, tense, person, number, formality="informal"):
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
lemma = lemma.strip().lower()
p, n = _PERSON.get(person), _NUMBER.get(number)
formal = "form" if formality == "formal" else "infm"
if p and n:
for fkey in (formal, "any", "infm" if formal == "form" else "form"):
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}|{fkey}"))
if form:
return form, "lexicon"
m = _mlc_conjugate(lemma, mood, tense, person, number)
if m:
return m, "model"
r = _rule_conjugate(lemma, mood, tense, person, number)
if r:
return r, "rule"
return lemma, "fallback"
_IRREG_PART = { # guarantee the common irregular participles
"escribir": "escrito", "describir": "descrito", "abrir": "abierto",
"cubrir": "cubierto", "descubrir": "descubierto", "morir": "muerto",
"poner": "puesto", "ver": "visto", "volver": "vuelto", "devolver": "devuelto",
"hacer": "hecho", "deshacer": "deshecho", "decir": "dicho", "romper": "roto",
"resolver": "resuelto", "freír": "frito", "imprimir": "impreso",
"satisfacer": "satisfecho", "prever": "previsto", "revolver": "revuelto",
}
def participle(lemma):
lemma = lemma.strip().lower()
if lemma in _IRREG_PART:
return _IRREG_PART[lemma], "lexicon"
if lemma in _PART:
return _PART[lemma], "lexicon"
if lemma.endswith("ar"):
return lemma[:-2] + "ado", "rule"
if lemma[-2:] in ("er", "ir"):
return lemma[:-2] + "ido", "rule"
return lemma, "fallback"
_IRREG_GER = {"dormir": "durmiendo", "morir": "muriendo", "pedir": "pidiendo",
"sentir": "sintiendo", "mentir": "mintiendo", "servir": "sirviendo",
"venir": "viniendo", "decir": "diciendo", "poder": "pudiendo",
"ir": "yendo", "leer": "leyendo", "creer": "creyendo",
"oír": "oyendo", "traer": "trayendo", "caer": "cayendo",
"construir": "construyendo", "huir": "huyendo", "reír": "riendo"}
def gerund(lemma):
lemma = lemma.strip().lower()
if lemma in _IRREG_GER:
return _IRREG_GER[lemma], "lexicon"
if lemma in _GER:
return _GER[lemma], "lexicon"
if lemma.endswith("ar"):
return lemma[:-2] + "ando", "rule"
if lemma[-2:] in ("er", "ir"):
return lemma[:-2] + "iendo", "rule"
return lemma, "fallback"
# ── PUBLIC: noun gender + number ────────────────────────────────────────────────
_INVARIANT_PL = {"lunes", "martes", "miércoles", "jueves", "viernes",
"crisis", "tesis", "análisis", "dosis", "virus", "paraguas"}
def _gender_heuristic(noun):
for suf, g in (("ión", "f"), ("dad", "f"), ("tad", "f"), ("umbre", "f"),
("sis", "f"), ("ez", "f"), ("triz", "f"),
("ema", "m"), ("ama", "m"), ("oma", "m"), ("aje", "m"),
("or", "m"), ("án", "m"), ("ín", "m")):
if noun.endswith(suf):
return g
if noun.endswith("o"):
return "m"
if noun.endswith("a"):
return "f"
return "m"
def noun_gender(lemma):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if d and d.get("g"):
return d["g"]
return _gender_heuristic(lemma)
def _regular_plural(noun):
if noun in _INVARIANT_PL:
return noun
if not noun:
return noun
last = noun[-1]
if last == "z":
return noun[:-1] + "ces"
if last in "aeiouáéíóú":
# stressed final vowel í/ú -> +es (rubí->rubíes), else +s
if last in "íú":
return noun + "es"
return noun + "s"
if last == "s":
# esdrújula / stress-final handled crudely; most polysyllables invariant
return noun
return noun + "es"
def inflect_noun(lemma, number, gender=None):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
num = "SG" if number == "singular" else "PL"
if d:
# honor a requested gender for animate nouns (gato -> gata)
if gender and (gender, num) in d:
return d[(gender, num)], "lexicon"
if d.get(num):
return d[num], "lexicon"
if number == "singular":
return lemma, "rule" if not d else "lexicon"
return _regular_plural(lemma), "rule"
# ── PUBLIC: adjective agreement ─────────────────────────────────────────────────
_INV_GENDER_ADJ = {"español": "española", "trabajador": "trabajadora",
"hablador": "habladora", "encantador": "encantadora",
"alemán": "alemana", "francés": "francesa", "inglés": "inglesa"}
def inflect_adj(lemma, gender, number):
lemma = lemma.strip().lower()
d = _ADJS.get(lemma)
num = "SG" if number == "singular" else "PL"
if d:
form = d.get((gender, num))
if form:
return form, "lexicon"
# gender-invariant adjective (grande, feliz, azul): fem == masc.
# For a missing plural, pluralize this gender's singular form.
sg = d.get((gender, "SG")) or d.get(("m", "SG")) or lemma
if number == "plural":
return _regular_plural(sg), "rule"
return sg, "lexicon"
# rule fallback
a = lemma
if gender == "f":
if a in _INV_GENDER_ADJ:
a = _INV_GENDER_ADJ[a]
elif a.endswith("o"):
a = a[:-1] + "a"
if number == "plural":
a = _regular_plural(a)
return a, ("rule" if (a != lemma or gender == "m") else "rule")
# ── PUBLIC: clitic enclisis (dá + me + lo -> dámelo) ────────────────────────────
def _strip_accents(s):
return "".join(c for c in unicodedata.normalize("NFD", s)
if unicodedata.category(c) != "Mn")
def _count_syllables_vowelgroups(word):
# crude: count vowel groups
w = _strip_accents(word).lower()
groups, prev = 0, False
for ch in w:
isv = ch in "aeiou"
if isv and not prev:
groups += 1
prev = isv
return groups
def _host_stress_from_end(word):
"""Stressed-syllable index counted from the end (1=last) of a verb host."""
syls = _count_syllables_vowelgroups(word)
if any(c in "áéíóú" for c in word):
return None # already carries its own accent
if word[-2:] in ("ar", "er", "ir"): # infinitive: oxytone
return 1
if word.endswith("ndo"): # gerund: paroxytone
return 2
if word[-1:] in "aeiouns" and syls >= 2: # default paroxytone
return 2
return 1 # monosyllable / consonant-final oxytone
def attach_enclitics(verb_form, clitics):
"""Append clitic pronouns to a verb (imperative/infinitive/gerund enclisis)
and add a written accent when the resulting word becomes esdrújula/
sobreesdrújula (stress >= 3 syllables from the end): dá+me+lo -> dámelo,
lleva+me -> llévame, but dar+te -> darte and da+me -> dame (no accent)."""
if not clitics:
return verb_form
tail = "".join(clitics)
if any(c in "áéíóú" for c in verb_form): # host already accented
return verb_form + tail
sfe = _host_stress_from_end(verb_form)
total_sfe = sfe + len(clitics) # each clitic = 1 syllable
if total_sfe >= 3:
return _accentuate_nucleus(verb_form, sfe) + tail
return verb_form + tail
def _accentuate_nucleus(word, sfe):
"""Put a written accent on the syllable `sfe` positions from the word's end."""
vowels = "aeiou"
nuclei = [i for i, ch in enumerate(word) if ch in vowels]
if not nuclei or sfe > len(nuclei):
return word
i = nuclei[-sfe]
acc = {"a": "á", "e": "é", "i": "í", "o": "ó", "u": "ú"}
return word[:i] + acc[word[i]] + word[i + 1:]
def _accentuate_last_stressed(word):
# Restore the host's ORIGINAL lexical stress with a written accent.
# Default Spanish stress: word ending in vowel/n/s -> penultimate syllable;
# otherwise (e.g. infinitives in -r) -> last syllable.
vowels = "aeiou"
nuclei = [i for i, ch in enumerate(word) if ch in vowels]
if not nuclei:
return word
if word[-1] in "aeiouns" and len(nuclei) >= 2:
i = nuclei[-2] # paroxytone: penult nucleus
else:
i = nuclei[-1] # oxytone / monosyllable: last nucleus
acc = {"a": "á", "e": "é", "i": "í", "o": "ó", "u": "ú"}
return word[:i] + acc[word[i]] + word[i + 1:]
def lexicon_stats():
return {
"source": "UniMorph Spanish (github.com/unimorph/spa)",
"license": "CC-BY-SA 3.0 (Wiktionary-derived)",
"total_forms": sum(len(v) for v in (_VERBS, _NOUNS, _ADJS)) if False else None,
"verb_forms": len(_VERBS),
"verb_lemmas": len({k[0] for k in _VERBS}),
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
"participles": len(_PART),
"gerunds": len(_GER),
}
if __name__ == "__main__":
import json
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
tests = [
("hablar", "ind", "present", "first", "singular", "hablo"),
("comer", "ind", "present", "third", "plural", "comen"),
("vivir", "ind", "present", "first", "plural", "vivimos"),
("ser", "ind", "present", "third", "singular", "es"),
("ir", "ind", "preterite", "first", "singular", "fui"),
("tener", "ind", "future", "first", "singular", "tendré"),
("hacer", "sbjv", "present", "first", "singular", "haga"),
("dormir", "ind", "present", "first", "singular", "duermo"),
("pensar", "sbjv", "present", "third", "singular", "piense"),
("dar", "ind", "preterite", "third", "singular", "dio"),
("poner", "ind", "conditional", "first", "singular", "pondría"),
]
ok = 0
for lemma, mood, tense, per, num, exp in tests:
got, conf = conjugate(lemma, mood, tense, per, num)
flag = "OK " if got == exp else "XX "
if got == exp:
ok += 1
print(f" {flag}{lemma:8} {mood}/{tense} {per[:3]}.{num[:2]:3} -> {got:14} ({conf}) exp={exp}")
print(f"verb tests {ok}/{len(tests)}")
print(" gender casa:", noun_gender("casa"), "| problema:", noun_gender("problema"),
"| agua:", noun_gender("agua"), "| mano:", noun_gender("mano"))
print(" plural: luz->", inflect_noun("luz", "plural"), "| rey->", inflect_noun("rey", "plural"))
print(" adj: rojo/f/pl->", inflect_adj("rojo", "f", "plural"),
"| feliz/m/pl->", inflect_adj("feliz", "m", "plural"),
"| grande/f/pl->", inflect_adj("grande", "f", "plural"))
print(" enclisis: da+[me,lo]->", attach_enclitics("da", ["me", "lo"]),
"| di+[me]->", attach_enclitics("di", ["me"]),
"| dar+[se,lo]->", attach_enclitics("dar", ["se", "lo"]))
-629
View File
@@ -1,629 +0,0 @@
"""morphology_fr_full.py — production-grade French morphological generator.
Same architecture as morphology_it_full.py (shared Romance engine); French-specific
data and rules swapped in. Backed by three real, Wiktionary-lineage sources:
VERBS
UniMorph French (github.com/unimorph/fra, CC-BY-SA 3.0)
7,535 verb lemmas × full paradigm, CLEAN orthography:
indicatif présent / imparfait (PST;IPFV) / passé simple (PST;PFV) /
futur, conditionnel (COND), subjonctif présent (SBJV;PRS) /
subjonctif imparfait (SBJV;PST), impératif (POS;IMP), infinitif (NFIN),
participe présent (V.CVB/V.PTCP;PRS), participe passé (V.PTCP;PST, m.sg).
fr_irreg_verbs.json — high-frequency verbs UniMorph MISSES or mis-slots,
above all ÊTRE (absent from UniMorph fra), plus avoir/aller/faire/… — the
auxiliaries the passé-composé + être-agreement system depends on. Extracted
from kaikki.org French (build_fr_irreg.py), reflexive/multiword forms
dropped. This layer takes PRIORITY.
NOUNS + ADJECTIVES — kaikki.org French (Wiktionary extract, CC-BY-SA 3.0)
noun lemmas WITH inherent gender (head-template arg) + real plural
(cheval->chevaux, œil->yeux, invariable -s/-x/-z), resolved PER LEMMA.
adjective lemmas with real feminine + plural (petit->petite/petits/petites,
beau->belle/beaux/belles, heureux->heureuse, rouge invariant-gender).
Fallbacks (degrade, never crash, on OOV input):
verbs : rule generator for -er / -ir(-iss-) / -re (with -cer/-ger spelling,
future/conditional stems, imparfait/subjonctif endings)
nouns : gender heuristic (endings) + rule pluralization (-al->-aux, -eau->-eaux)
adjs : fem/plural agreement rules (-er->-ère, -eux->-euse, -f->-ve, +e default)
Confidence flag on every form: "lexicon" | "rule" | "fallback".
Public API (used by realizer_fr.py): identical signature to morphology_it_full.
"""
import json
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "fra.unimorph")
_IRREG = os.path.join(_HERE, "data", "fr_irreg_verbs.json")
_KAIKKI = os.path.join(_HERE, "data", "kaikki_fr.jsonl")
_CACHE = os.path.join(_HERE, "data", "fr_morph_cache.pkl")
# ── (mood, tense) -> UniMorph feature set that must ALL be present ────────────────
_VERB_KEYMAP = {
("ind", "present"): {"IND", "PRS"},
("ind", "imperfect"): {"IND", "PST", "IPFV"}, # imparfait
("ind", "passe_simple"): {"IND", "PST", "PFV"}, # passé simple
("ind", "future"): {"IND", "FUT"},
("ind", "conditional"): {"COND"}, # French: V;COND;1;SG
("sbjv", "present"): {"SBJV", "PRS"},
("sbjv", "imperfect"): {"SBJV", "PST"},
("imp", "affirmative"): {"POS", "IMP"},
}
_PERSON = {"first": "1", "second": "2", "third": "3"}
_NUMBER = {"singular": "SG", "plural": "PL"}
def _feat_set(tag):
return set(tag.split(";"))
# ── build verb lexicon from UniMorph ─────────────────────────────────────────────
def _build_verbs():
verbs = {}
part = {}
ger = {}
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
f = _feat_set(tag)
head = tag.split(";")[0]
if head == "V.PTCP":
if "PST" in f:
part.setdefault(lemma, form)
elif "PRS" in f:
ger.setdefault(lemma, form)
continue
if head == "V.CVB":
if "PRS" in f:
ger.setdefault(lemma, form)
continue
if head != "V":
continue
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person is None or number is None:
continue
for (mood, tense), req in _VERB_KEYMAP.items():
if not req <= f:
continue
if tense == "imperfect" and "PFV" in f:
continue
if tense == "passe_simple" and "IPFV" in f:
continue
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
break
return verbs, part, ger
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
"diminutive", "augmentative", "pejorative", "comparative",
"superlative", "misspelling", "rare", "informal", "literary",
"poetic", "error-unrecognized-form", "construed", "collective",
"nonstandard", "dated", "Louisiana", "Switzerland", "Belgium"}
def _kaikki_gender(arg):
if not arg:
return None
a = str(arg).lower()
if a.startswith("f"):
return "f"
if a.startswith("m"):
return "m"
return None
def _build_nouns_adjs():
nouns = {}
adjs = {}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
pos = d.get("pos")
word = d.get("word", "")
if not word or " " in word:
continue
forms = d.get("forms", []) or []
if pos == "noun":
ht = d.get("head_templates") or []
g = None
if ht:
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
if g is None:
tags = d.get("tags") or []
if "feminine" in tags:
g = "f"
elif "masculine" in tags:
g = "m"
pl = None
for x in forms:
t = set(x.get("tags") or [])
if "plural" in t and not (t & _EXCL_FORM_TAGS):
fm = x.get("form")
if fm and " " not in fm and fm not in ("#", "-", ""):
pl = fm
break
if word not in nouns:
nouns[word] = {"g": g, "SG": word, "PL": pl}
else:
cur = nouns[word]
if cur.get("g") is None and g:
cur["g"] = g
if not cur.get("PL") and pl:
cur["PL"] = pl
elif pos == "adj":
d0 = adjs.setdefault(word, {})
d0.setdefault(("m", "SG"), word)
for x in forms:
t = set(x.get("tags") or [])
fm = x.get("form")
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
continue
if "feminine" in t and "plural" in t:
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
elif "masculine" in t and "plural" in t:
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
elif "feminine" in t:
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
elif "plural" in t:
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
return nouns, adjs
def _build_cache():
verbs, part, ger = _build_verbs()
nouns, adjs = _build_nouns_adjs()
with open(_IRREG, encoding="utf-8") as fh:
irreg = json.load(fh)
data = {"verbs": verbs, "part": part, "ger": ger,
"nouns": nouns, "adjs": adjs, "irreg": irreg}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE):
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
if os.path.getmtime(_CACHE) >= newest:
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
_LEX["irreg"])
# ── regular-ending rule fallback ─────────────────────────────────────────────────
def _vclass(lemma):
if lemma.endswith("er"):
return "er"
if lemma.endswith("ir"):
return "ir"
if lemma.endswith("re"):
return "re"
if lemma.endswith("oir"):
return "oir"
return None
# present-tense endings [1sg,2sg,3sg,1pl,2pl,3pl]
_REG_PRES = {
"er": ["e", "es", "e", "ons", "ez", "ent"],
"ir": ["is", "is", "it", "issons", "issez", "issent"], # -iss- class (finir)
"re": ["s", "s", "", "ons", "ez", "ent"], # vendre: vends/vend
}
_REG_IMPF = ["ais", "ais", "ait", "ions", "iez", "aient"] # attaches to pres-1pl stem
_REG_SUBJ = ["e", "es", "e", "ions", "iez", "ent"] # attaches to 3pl stem
_REG_PS = { # passé simple
"er": ["ai", "as", "a", "âmes", "âtes", "èrent"],
"ir": ["is", "is", "it", "îmes", "îtes", "irent"],
"re": ["is", "is", "it", "îmes", "îtes", "irent"],
}
_FUT = ["ai", "as", "a", "ons", "ez", "ont"]
_COND = ["ais", "ais", "ait", "ions", "iez", "aient"]
def _slot_idx(person, number):
base = {"first": 0, "second": 1, "third": 2}[person]
return base + (0 if number == "singular" else 3)
def _fut_stem(lemma, vc):
"""Future/conditional stem = infinitive (drop final -e of -re)."""
if vc == "re":
return lemma[:-1] # vendre -> vendr-
return lemma # parler-, finir-
def _pres_1pl_stem(lemma, vc):
"""Imparfait stem = present 1pl minus -ons (parlons->parl-, finissons->finiss-)."""
if vc == "er":
stem = lemma[:-2]
if stem.endswith("g"):
return stem + "e" # mangeons -> mange- (imparfait mangeais)
if stem.endswith("c"):
return stem[:-1] + "ç" # commençons -> commenç-
return stem
if vc == "ir":
return lemma[:-1] + "iss" # finir -> finiss-
if vc == "re":
return lemma[:-2] # vendre -> vend-
return lemma[:-2]
def _apply_er_spelling(stem, ending):
"""-cer/-ger softening before a/o (commençons, mangeons)."""
if ending and ending[0] in ("a", "o"):
if stem.endswith("c"):
return stem[:-1] + "ç" + ending
if stem.endswith("g"):
return stem + "e" + ending
return stem + ending
def _rule_conjugate(lemma, mood, tense, person, number):
vc = _vclass(lemma)
if vc is None:
return None
i = _slot_idx(person, number)
if mood == "ind" and tense in ("future", "conditional"):
stem = _fut_stem(lemma, vc)
end = (_FUT if tense == "future" else _COND)[i]
return stem + end
if mood == "ind" and tense == "present":
table = _REG_PRES.get("ir" if vc == "ir" else vc)
if not table:
return None
body = lemma[:-2] if vc in ("er", "re") else lemma[:-1] if vc == "ir" else lemma[:-2]
if vc == "ir":
body = lemma[:-2] # fin- ; endings carry -iss-
end = table[i]
return body + end
end = table[i]
if vc == "er":
return _apply_er_spelling(body, end)
return body + end
if mood == "ind" and tense == "imperfect":
stem = _pres_1pl_stem(lemma, vc)
return stem + _REG_IMPF[i]
if mood == "ind" and tense == "passe_simple":
table = _REG_PS.get("ir" if vc == "ir" else vc)
if not table:
return None
body = lemma[:-2] if vc in ("er", "re") else lemma[:-2]
end = table[i]
if vc == "er":
return _apply_er_spelling(body, end)
return body + end
if mood == "sbjv" and tense == "present":
# subjonctif: present-3pl stem + e/es/e/ions/iez/ent
stem3 = _pres_1pl_stem(lemma, vc) if vc == "ir" else (
lemma[:-2] if vc in ("er", "re") else lemma[:-2])
if vc == "ir":
stem3 = lemma[:-2] + "iss"
end = _REG_SUBJ[i]
if vc == "er":
return _apply_er_spelling(stem3, end)
return stem3 + end
if mood == "imp" and tense == "affirmative":
# impératif ~ present indicative (tu drops -s for -er verbs)
pres = _rule_conjugate(lemma, "ind", "present", person, number)
if pres and vc == "er" and person == "second" and number == "singular":
return pres[:-1] if pres.endswith("es") else pres
return pres
return None
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
def conjugate(lemma, mood, tense, person, number):
"""Return (surface, confidence)."""
lemma = lemma.strip().lower()
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number}"
ir = _IRREGV.get(lemma)
if ir and key in ir:
return ir[key], "lexicon"
p, n = _PERSON.get(person), _NUMBER.get(number)
if p and n:
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
if form:
return form, "lexicon"
r = _rule_conjugate(lemma, mood, tense, person, number)
if r:
return r, "rule"
return lemma, "fallback"
# ── PUBLIC: participle + gerund/participe présent ────────────────────────────────
def _participle_msg(lemma):
ir = _IRREGV.get(lemma)
if ir and "part" in ir:
return ir["part"], "lexicon"
if lemma in _PART:
return _PART[lemma], "lexicon"
return None, None
# irregular participle fem/plural quirks (drop circonflexe: dû->due, dus)
_PART_FIX = {"": {"f|SG": "due", "m|PL": "dus", "f|PL": "dues"}}
def participle(lemma, gender="m", number="singular"):
"""Past participle with French gender/number agreement.
m.sg = base; f.sg = base+e; m.pl = base+s (invariable if base ends s/x);
f.pl = f.sg+s."""
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
msg, src = _participle_msg(lemma)
conf = "lexicon"
if msg is None:
vc = _vclass(lemma)
if vc == "er":
msg = lemma[:-2] + "é"
elif vc == "ir":
msg = lemma[:-1] # finir -> fini, partir -> parti
elif vc == "re":
msg = lemma[:-2] + "u" # vendre -> vendu
elif vc == "oir":
msg = lemma[:-3] + "u" # (rough) recevoir handled by irreg
else:
return lemma, "fallback"
conf = "rule"
fix = _PART_FIX.get(msg)
if fix and f"{g}|{num}" in fix:
return fix[f"{g}|{num}"], conf
if g == "m" and num == "SG":
return msg, conf
fem = msg + "e" if not msg.endswith("e") else msg
if g == "f" and num == "SG":
return fem, conf
if g == "m" and num == "PL":
return msg if msg.endswith(("s", "x")) else msg + "s", conf
# f|PL
return fem + "s", conf
def gerund(lemma):
"""Participe présent (base for gérondif 'en -ant')."""
lemma = lemma.strip().lower()
ir = _IRREGV.get(lemma)
if ir and "ger" in ir:
return ir["ger"], "lexicon"
if lemma in _GER:
return _GER[lemma], "lexicon"
vc = _vclass(lemma)
if vc == "er":
stem = lemma[:-2]
if stem.endswith("g"):
return stem + "eant", "rule"
if stem.endswith("c"):
return stem[:-1] + "çant", "rule"
return stem + "ant", "rule"
if vc == "ir":
return lemma[:-2] + "issant", "rule"
if vc == "re":
return lemma[:-2] + "ant", "rule"
return lemma, "fallback"
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
_FEM_SUF = ("tion", "sion", "aison", "ance", "ence", "ette", "elle", "esse",
"ude", "ade", "ée", "", "tié", "ie", "ise", "ure", "eur")
_MASC_SUF = ("ment", "age", "eau", "isme", "oir", "ier", "eur", "in", "on")
def _gender_heuristic(noun):
for suf in _FEM_SUF:
if noun.endswith(suf):
return "f"
for suf in _MASC_SUF:
if noun.endswith(suf):
return "m"
if noun.endswith("e"):
return "f"
return "m"
def noun_gender(lemma):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if d and d.get("g") in ("m", "f"):
return d["g"]
return _gender_heuristic(lemma)
# closed sets for French plural irregularities
_OU_X = {"bijou", "caillou", "chou", "genou", "hibou", "joujou", "pou"}
_AIL_AUX = {"travail", "vitrail", "corail", "émail", "bail", "soupirail", "vantail"}
_AL_S = {"bal", "carnaval", "festival", "récital", "chacal", "régal", "cal", "aval"}
def _rule_plural(noun, gender):
"""Deterministic French pluralization. (form, ok); ok=False FLAGS ambiguity."""
if not noun:
return noun, True
if noun[-1:] in ("s", "x", "z"):
return noun, True # invariable
if noun in _OU_X:
return noun + "x", True
if noun.endswith(("eau", "au", "eu")):
if noun in ("pneu", "bleu", "landau", "sarrau"):
return noun + "s", True
return noun + "x", True # bateau->bateaux, jeu->jeux
if noun.endswith("al"):
if noun in _AL_S:
return noun + "s", True
return noun[:-2] + "aux", True # cheval->chevaux
if noun.endswith("ail"):
if noun in _AIL_AUX:
return noun[:-3] + "aux", True # travail->travaux
return noun + "s", True
return noun + "s", True # default
def inflect_noun(lemma, number, gender=None):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if number == "singular":
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
if d and d.get("PL"):
return d["PL"], "lexicon"
g = gender or noun_gender(lemma)
form, ok = _rule_plural(lemma, g)
return form, ("rule" if ok else "fallback")
# adjectives whose kaikki entries are unreliable: audited forms
_ADJ_FIX = {
"beau": {("m", "SG"): "beau", ("f", "SG"): "belle",
("m", "PL"): "beaux", ("f", "PL"): "belles"},
"nouveau": {("m", "SG"): "nouveau", ("f", "SG"): "nouvelle",
("m", "PL"): "nouveaux", ("f", "PL"): "nouvelles"},
"vieux": {("m", "SG"): "vieux", ("f", "SG"): "vieille",
("m", "PL"): "vieux", ("f", "PL"): "vieilles"},
"fou": {("m", "SG"): "fou", ("f", "SG"): "folle",
("m", "PL"): "fous", ("f", "PL"): "folles"},
"blanc": {("m", "SG"): "blanc", ("f", "SG"): "blanche",
("m", "PL"): "blancs", ("f", "PL"): "blanches"},
"long": {("m", "SG"): "long", ("f", "SG"): "longue",
("m", "PL"): "longs", ("f", "PL"): "longues"},
"bon": {("m", "SG"): "bon", ("f", "SG"): "bonne",
("m", "PL"): "bons", ("f", "PL"): "bonnes"},
}
def _rule_fem(a):
if a.endswith("e"):
return a
if a.endswith("er"):
return a[:-2] + "ère"
if a.endswith("eau"):
return a[:-3] + "elle"
if a.endswith("eux"):
return a[:-3] + "euse"
if a.endswith("f"):
return a[:-1] + "ve"
if a.endswith(("on", "en", "el", "eil", "et")):
return a + a[-1] + "e" # bon->bonne, ancien->ancienne, muet->muette
if a.endswith("c"):
return a[:-1] + "che" # blanc->blanche (public->publique via FIX)
return a + "e" # grand->grande, petit->petite, vert->verte
def inflect_adj(lemma, gender, number):
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
fix = _ADJ_FIX.get(lemma)
if fix and (g, num) in fix:
return fix[(g, num)], "lexicon"
d = _ADJS.get(lemma)
if d and d.get((g, num)):
return d[(g, num)], "lexicon"
# derive
msc = (d.get(("m", "SG")) if d else None) or lemma
if g == "m" and num == "SG":
return msc, "lexicon" if d else "rule"
fem = (d.get(("f", "SG")) if d else None) or _rule_fem(msc)
if g == "f" and num == "SG":
return fem, "lexicon" if (d and d.get(("f", "SG"))) else "rule"
if g == "m" and num == "PL":
if msc.endswith(("s", "x")):
return msc, "rule"
if msc.endswith("al"):
return msc[:-2] + "aux", "rule"
if msc.endswith("eau"):
return msc + "x", "rule"
return msc + "s", "rule"
# f|PL
return (fem if fem.endswith("s") else fem + "s"), "rule"
def lexicon_stats():
return {
"verb_source": "UniMorph French (github.com/unimorph/fra) + kaikki.org "
"irregulars (être + high-frequency)",
"noun_adj_source": "kaikki.org French (Wiktionary extract)",
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
"unimorph_verb_forms": len(_VERBS),
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
"irregular_verb_lemmas": len(_IRREGV),
"participle_lemmas": len(_PART),
"gerund_lemmas": len(_GER),
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
}
if __name__ == "__main__":
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
tests = [
("parler", "ind", "present", "first", "singular", "parle"),
("être", "ind", "present", "third", "singular", "est"),
("avoir", "ind", "present", "first", "singular", "ai"),
("aller", "ind", "present", "third", "plural", "vont"),
("finir", "ind", "present", "first", "singular", "finis"),
("finir", "ind", "present", "first", "plural", "finissons"),
("manger", "ind", "present", "first", "plural", "mangeons"),
("faire", "ind", "future", "first", "singular", "ferai"),
("pouvoir", "sbjv", "present", "third", "singular", "puisse"),
("prendre", "ind", "passe_simple", "third", "singular", "prit"),
("vendre", "ind", "present", "third", "singular", "vend"),
("commencer", "ind", "imperfect", "first", "singular", "commençais"),
]
ok = 0
for lemma, mood, tense, per, num, exp in tests:
got, conf = conjugate(lemma, mood, tense, per, num)
flag = "OK " if got == exp else "XX "
ok += got == exp
print(f" {flag}{lemma:10} {mood}/{tense:12} {per[:3]}.{num[:2]} -> {got:12} ({conf}) exp={exp}")
print(f"verb tests {ok}/{len(tests)}")
print(" gender: maison=", noun_gender("maison"), "chat=", noun_gender("chat"),
"cheval=", noun_gender("cheval"), "nation=", noun_gender("nation"))
print(" plural: cheval->", inflect_noun("cheval", "plural"),
"| bateau->", inflect_noun("bateau", "plural"),
"| prix->", inflect_noun("prix", "plural"),
"| chat->", inflect_noun("chat", "plural"))
print(" adj: petit/f/sg->", inflect_adj("petit", "f", "singular"),
"| beau/f/sg->", inflect_adj("beau", "f", "singular"),
"| heureux/f/sg->", inflect_adj("heureux", "f", "singular"),
"| national/m/pl->", inflect_adj("national", "m", "plural"))
print(" part: aller/f/sg->", participle("aller", "f", "singular"),
"| prendre/f/pl->", participle("prendre", "f", "plural"),
"| finir/m/pl->", participle("finir", "m", "plural"))
print(" ger: manger->", gerund("manger"), "| finir->", gerund("finir"))
-588
View File
@@ -1,588 +0,0 @@
"""morphology_it_full.py — production-grade Italian morphological generator.
NOT a toy. Backed by three real, Wiktionary-lineage lexical sources:
VERBS
UniMorph Italian (github.com/unimorph/ita, CC-BY-SA 3.0)
10,009 verb lemmas × full paradigm, CLEAN orthography (no stress marks):
indicative present / imperfetto (PST;IPFV) / passato remoto (PST;PFV) /
futuro, condizionale (COND),
congiuntivo presente (SBJV;PRS) / imperfetto (SBJV;PST),
affirmative imperative, infinitive, gerundio (V.CVB;PRS),
past participle (masc-sg; fem/plural derived by vowel rule).
it_irreg_verbs.json 66 high-frequency verbs UniMorph MISSES
(essere, avere, potere, uscire, tenere, prendere, piacere, ), extracted
from kaikki.org Italian, filtered to standard forms, and DE-STRESSED to
real orthography (kaikki marks tonic stress everywhere: pàrlo->parlo,
avùto->avuto; final legit accents kept: sarò, è). Built by build_it_irreg.py.
This layer takes priority it supplies the two auxiliaries essere/avere,
which the whole passato-prossimo / essere-agreement system depends on.
NOUNS + ADJECTIVES kaikki.org Italian (Wiktionary extract, CC-BY-SA 3.0)
noun lemmas WITH inherent gender (head-template arg) + real (often irregular)
plural uomo->uomini, uovo->uova, dito->dita, città invariant resolved
PER LEMMA, never guessed.
adjective lemmas with real feminine + masc/fem plural (italiano->italiana/
italiani/italiane, felice->felici invariant).
Fallbacks (degrade, never crash, on OOV input):
verbs : rule generator for regular -are/-ere/-ire (with -care/-gare h-insertion
and -ciare/-giare/-iare i-drop spelling rules)
nouns : gender heuristic (endings) + rule pluralization (ambiguous -co/-go FLAGGED)
adjs : -o/-a/-e gender rule + rule pluralization
Confidence flag on every form:
"lexicon" from UniMorph / kaikki-irregular / kaikki noun-adj (trust: high)
"rule" deterministic rule (trust: medium)
"fallback" could not inflect; returned lemma / ambiguous (trust: low -> FLAG)
Public API (used by realizer_it.py):
conjugate(lemma, mood, tense, person, number) -> (form, conf)
participle(lemma, gender="m", number="singular") -> (form, conf)
gerund(lemma) -> (form, conf)
noun_gender(lemma) -> "m"|"f"
inflect_noun(lemma, number, gender=None) -> (form, conf)
inflect_adj(lemma, gender, number) -> (form, conf)
lexicon_stats() -> dict
"""
import json
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "ita.unimorph")
_IRREG = os.path.join(_HERE, "data", "it_irreg_verbs.json")
_KAIKKI = os.path.join(_HERE, "data", "kaikki_it.jsonl")
_CACHE = os.path.join(_HERE, "data", "it_morph_cache.pkl")
# ── (mood, tense) -> UniMorph feature set that must ALL be present ────────────────
_VERB_KEYMAP = {
("ind", "present"): {"IND", "PRS"},
("ind", "imperfect"): {"IND", "PST", "IPFV"},
("ind", "passato_remoto"): {"IND", "PST", "PFV"},
("ind", "future"): {"IND", "FUT"},
("ind", "conditional"): {"COND"},
("sbjv", "present"): {"SBJV", "PRS"},
("sbjv", "imperfect"): {"SBJV", "PST"},
("imp", "affirmative"): {"POS", "IMP"},
}
_PERSON = {"first": "1", "second": "2", "third": "3"}
_NUMBER = {"singular": "SG", "plural": "PL"}
def _feat_set(tag):
return set(tag.split(";"))
# ── build verb lexicon from UniMorph ─────────────────────────────────────────────
def _build_verbs():
verbs = {} # (lemma, "mood|tense|person|number") -> form
part = {} # lemma -> masc-sg past participle
ger = {} # lemma -> gerundio
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
f = _feat_set(tag)
head = tag.split(";")[0]
if head == "V.PTCP":
if "PST" in f:
part.setdefault(lemma, form)
continue
if head == "V.CVB": # gerundio (converb, present)
if "PRS" in f:
ger.setdefault(lemma, form)
continue
if head != "V":
continue
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person is None or number is None:
continue
for (mood, tense), req in _VERB_KEYMAP.items():
# exact-set discipline: PST;PFV must not match PST;IPFV, etc.
if not req <= f:
continue
# guard IND;PST ambiguity: require the specific aspect feature
if tense == "imperfect" and "PFV" in f:
continue
if tense == "passato_remoto" and "IPFV" in f:
continue
# COND must not also be a subjunctive/imperative slot
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
break
return verbs, part, ger
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
"diminutive", "augmentative", "pejorative", "comparative",
"superlative", "misspelling", "rare", "informal", "literary",
"poetic", "error-unrecognized-form", "apocopic", "obsolete",
"construed", "collective"}
def _kaikki_gender(arg):
if not arg:
return None
a = str(arg).lower()
if a.startswith("f"):
return "f"
if a.startswith("m"):
return "m"
return None
def _build_nouns_adjs():
nouns = {} # lemma -> {"g","SG","PL"}
adjs = {} # lemma -> {("m","SG"),("f","SG"),("m","PL"),("f","PL")}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
pos = d.get("pos")
word = d.get("word", "")
if not word or " " in word:
continue
forms = d.get("forms", []) or []
if pos == "noun":
ht = d.get("head_templates") or []
g = None
if ht:
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
if g is None:
tags = d.get("tags") or []
if "feminine" in tags:
g = "f"
elif "masculine" in tags:
g = "m"
pl = None
for x in forms:
t = set(x.get("tags") or [])
if "plural" in t and not (t & _EXCL_FORM_TAGS):
fm = x.get("form")
if fm and " " not in fm and fm != "#":
pl = fm
break
if word not in nouns:
nouns[word] = {"g": g, "SG": word, "PL": pl}
else:
cur = nouns[word]
if cur.get("g") is None and g:
cur["g"] = g
if not cur.get("PL") and pl:
cur["PL"] = pl
elif pos == "adj":
d0 = adjs.setdefault(word, {})
d0.setdefault(("m", "SG"), word)
for x in forms:
t = set(x.get("tags") or [])
fm = x.get("form")
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
continue
if "feminine" in t and "plural" in t:
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
elif "masculine" in t and "plural" in t:
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
elif "feminine" in t:
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
elif "plural" in t: # invariant-gender adj (felice -> felici)
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
return nouns, adjs
def _build_cache():
verbs, part, ger = _build_verbs()
nouns, adjs = _build_nouns_adjs()
with open(_IRREG, encoding="utf-8") as fh:
irreg = json.load(fh)
data = {"verbs": verbs, "part": part, "ger": ger,
"nouns": nouns, "adjs": adjs, "irreg": irreg}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE):
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
if os.path.getmtime(_CACHE) >= newest:
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
_LEX["irreg"])
# ── regular-ending rule fallback ─────────────────────────────────────────────────
def _vclass(lemma):
if lemma.endswith("are"):
return "are"
if lemma.endswith("ere"):
return "ere"
if lemma.endswith("ire"):
return "ire"
return None
# endings [1sg,2sg,3sg,1pl,2pl,3pl]
_REG = {
("ind", "present", "are"): ["o", "i", "a", "iamo", "ate", "ano"],
("ind", "present", "ere"): ["o", "i", "e", "iamo", "ete", "ono"],
("ind", "present", "ire"): ["o", "i", "e", "iamo", "ite", "ono"],
("ind", "imperfect", "are"): ["avo", "avi", "ava", "avamo", "avate", "avano"],
("ind", "imperfect", "ere"): ["evo", "evi", "eva", "evamo", "evate", "evano"],
("ind", "imperfect", "ire"): ["ivo", "ivi", "iva", "ivamo", "ivate", "ivano"],
("ind", "passato_remoto", "are"): ["ai", "asti", "ò", "ammo", "aste", "arono"],
("ind", "passato_remoto", "ere"): ["ei", "esti", "é", "emmo", "este", "erono"],
("ind", "passato_remoto", "ire"): ["ii", "isti", "ì", "immo", "iste", "irono"],
("sbjv", "present", "are"): ["i", "i", "i", "iamo", "iate", "ino"],
("sbjv", "present", "ere"): ["a", "a", "a", "iamo", "iate", "ano"],
("sbjv", "present", "ire"): ["a", "a", "a", "iamo", "iate", "ano"],
("sbjv", "imperfect", "are"): ["assi", "assi", "asse", "assimo", "aste", "assero"],
("sbjv", "imperfect", "ere"): ["essi", "essi", "esse", "essimo", "este", "essero"],
("sbjv", "imperfect", "ire"): ["issi", "issi", "isse", "issimo", "iste", "issero"],
# imperative: 2sg,3sg(Lei),1pl,2pl,3pl (1sg has none)
("imp", "affirmative", "are"): [None, "a", "i", "iamo", "ate", "ino"],
("imp", "affirmative", "ere"): [None, "i", "a", "iamo", "ete", "ano"],
("imp", "affirmative", "ire"): [None, "i", "a", "iamo", "ite", "ano"],
}
# future / conditional attach to a stem = infinitive minus final -e, with
# -are -> -er (parlare->parler-), -ere/-ire keep (credere->creder-, dormir-)
_FUT = ["ò", "ai", "à", "emo", "ete", "anno"]
_COND = ["ei", "esti", "ebbe", "emmo", "este", "ebbero"]
def _slot_idx(person, number):
base = {"first": 0, "second": 1, "third": 2}[person]
return base + (0 if number == "singular" else 3)
def _fut_stem(lemma, vc):
body = lemma[:-3] # drop are/ere/ire
if vc == "are":
return body + "er"
return body + vc[0] + "r" # ere->er? no: keep vowel: creder-, dormir-
# NOTE corrected below
def _apply_are_spelling(stem, ending):
"""-care/-gare insert h before front endings; -ciare/-giare/-sciare/-iare drop i."""
front = ending[:1] in ("i", "e")
if stem.endswith(("c", "g")) and front:
return stem + "h" + ending
if stem.endswith(("ci", "gi", "sci")) and ending[:1] == "i":
return stem[:-1] + ending # mangi+iamo -> mangiamo
if stem.endswith("i") and ending[:1] == "i":
return stem[:-1] + ending # studi+iamo -> studiamo
return stem + ending
def _rule_conjugate(lemma, mood, tense, person, number):
vc = _vclass(lemma)
if vc is None:
return None
body = lemma[:-3]
i = _slot_idx(person, number)
if mood == "ind" and tense in ("future", "conditional"):
stem = body + "er" if vc == "are" else body + vc[0] + "r"
# ere: creder-, ire: dormir- -> body + 'e'/'i' + 'r'
if vc == "ere":
stem = body + "er"
elif vc == "ire":
stem = body + "ir"
end = (_FUT if tense == "future" else _COND)[i]
# spelling: -care/-gare -> cherò/gherò ; -ciare/-giare -> cerò/gerò
if vc == "are":
if body.endswith(("c", "g")):
stem = body + "her"
elif body.endswith(("ci", "gi", "sci")):
stem = body[:-1] + "er"
elif body.endswith("i"):
stem = body[:-1] + "er"
return stem + end
table = _REG.get((mood, tense, vc))
if not table:
return None
end = table[i]
if end is None:
return None
if vc == "are":
return _apply_are_spelling(body, end)
# -ere/-ire: guard against double-i (dormi+iamo -> dormiamo)
if body.endswith("i") and end[:1] == "i":
return body[:-1] + end
return body + end
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
def conjugate(lemma, mood, tense, person, number):
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
lemma = lemma.strip().lower()
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number}"
ir = _IRREGV.get(lemma)
if ir and key in ir:
return ir[key], "lexicon"
p, n = _PERSON.get(person), _NUMBER.get(number)
if p and n:
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
if form:
return form, "lexicon"
r = _rule_conjugate(lemma, mood, tense, person, number)
if r:
return r, "rule"
return lemma, "fallback"
# ── PUBLIC: participle + gerund ──────────────────────────────────────────────────
def _participle_msg(lemma):
"""Return (masc-sg participle, source) or (None, None)."""
ir = _IRREGV.get(lemma)
if ir and "part" in ir:
return ir["part"], "lexicon"
if lemma in _PART:
return _PART[lemma], "lexicon"
return None, None
def participle(lemma, gender="m", number="singular"):
"""Past participle with gender/number agreement (for essere-perfect & passives).
UniMorph/irregular give masc-sg; fem/plural derived by final-vowel swap
(-o -> -a/-i/-e), valid for regular -ato/-uto/-ito AND irregulars
(preso->presa/presi/prese, aperto->aperta/aperti/aperte, morto->morta/...)."""
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
msg, src = _participle_msg(lemma)
conf = "lexicon"
if msg is None:
vc = _vclass(lemma)
if vc == "are":
msg = lemma[:-3] + "ato"
elif vc == "ere":
msg = lemma[:-3] + "uto"
elif vc == "ire":
msg = lemma[:-3] + "ito"
else:
return lemma, "fallback"
conf = "rule"
# agreement: only -o participles inflect for gender+number
if msg.endswith("o"):
stem = msg[:-1]
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "i", "f|PL": "e"}[f"{g}|{num}"]
return stem + suf, conf
return msg, conf # non -o participle: leave as-is (rare)
def gerund(lemma):
lemma = lemma.strip().lower()
ir = _IRREGV.get(lemma)
if ir and "ger" in ir:
return ir["ger"], "lexicon"
if lemma in _GER:
return _GER[lemma], "lexicon"
vc = _vclass(lemma)
if vc == "are":
return lemma[:-3] + "ando", "rule"
if vc in ("ere", "ire"):
return lemma[:-3] + "endo", "rule"
return lemma, "fallback"
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
_FEM_SUF = ("zione", "sione", "gione", "", "", "trice", "aggine", "udine",
"igine", "ie", "essa", "izia", "ezza")
_MASC_SUF = ("ore", "ame", "iere", "ale", "ile")
def _gender_heuristic(noun):
for suf in _FEM_SUF:
if noun.endswith(suf):
return "f"
for suf in _MASC_SUF:
if noun.endswith(suf):
return "m"
if noun.endswith("o"):
return "m"
if noun.endswith("a"):
return "f"
if noun.endswith("à") or noun.endswith("ù"):
return "f"
return "m" # -e and consonant-final loanwords default masculine
def noun_gender(lemma):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if d and d.get("g") in ("m", "f"):
return d["g"]
return _gender_heuristic(lemma)
def _rule_plural(noun, gender):
"""Deterministic Italian pluralization. Returns (form, ok); ok=False FLAGS an
ambiguous case the lexicon would normally resolve (-co/-go palatalization)."""
if not noun:
return noun, True
# invariant: accented final vowel, consonant-final, monosyllable, -i final
if noun[-1:] in ("à", "è", "é", "ì", "í", "ò", "ó", "ù", "ú"):
return noun, True
if noun[-1:] not in ("a", "e", "o", "i", "u"):
return noun, True # consonant-final loanword: invariant
if noun.endswith("i"):
return noun, True # e.g. crisi, analisi: invariant
if noun.endswith("io"):
return noun[:-2] + "i", True # figlio->figli (unstressed i)
if noun.endswith("cia") or noun.endswith("gia"):
# vowel before cia/gia -> -cie/-gie ; consonant -> -ce/-ge (approx)
return noun[:-2] + "e", True # arancia->arance (majority)
if noun.endswith("ca"):
return noun[:-2] + "che", True # amica->amiche
if noun.endswith("ga"):
return noun[:-2] + "ghe", True
if noun.endswith("co"):
return noun[:-2] + "chi", False # AMBIGUOUS (amico->amici) -> flag
if noun.endswith("go"):
return noun[:-2] + "ghi", False # AMBIGUOUS (psicologo->psicologi)
if noun.endswith("a"):
return noun[:-1] + "e", True # casa->case (m -a: -i, but rare)
if noun.endswith("o"):
return noun[:-1] + "i", True # libro->libri
if noun.endswith("e"):
return noun[:-1] + "i", True # cane->cani, chiave->chiavi
return noun, True
def inflect_noun(lemma, number, gender=None):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if number == "singular":
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
if d and d.get("PL"):
return d["PL"], "lexicon"
g = gender or noun_gender(lemma)
form, ok = _rule_plural(lemma, g)
return form, ("rule" if ok else "fallback")
# adjectives whose kaikki entries are unreliable (messy inflection templates):
# supply audited regular agreement forms (prenominal apocope handled in realizer).
_ADJ_FIX = {
"bello": {("m", "SG"): "bello", ("f", "SG"): "bella",
("m", "PL"): "belli", ("f", "PL"): "belle"},
"quello": {("m", "SG"): "quello", ("f", "SG"): "quella",
("m", "PL"): "quelli", ("f", "PL"): "quelle"},
}
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
def inflect_adj(lemma, gender, number):
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
fix = _ADJ_FIX.get(lemma)
if fix and (g, num) in fix:
return fix[(g, num)], "lexicon"
d = _ADJS.get(lemma)
if d:
form = d.get((g, num))
if form:
return form, "lexicon"
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
if num == "PL":
pl, ok = _rule_plural(sg, g)
return pl, ("rule" if ok else "fallback")
return sg, "lexicon"
# rule fallback
a = lemma
if a.endswith("o"): # -o/-a/-i/-e class
base = a[:-1]
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "i", "f|PL": "e"}[f"{g}|{num}"]
return base + suf, "rule"
if a.endswith("e"): # felice-class: SG invariant, PL -i
if num == "PL":
return a[:-1] + "i", "rule"
return a, "rule"
if num == "PL":
p, ok = _rule_plural(a, g)
return p, ("rule" if ok else "fallback")
return a, "rule"
def lexicon_stats():
return {
"verb_source": "UniMorph Italian (github.com/unimorph/ita) + kaikki.org "
"irregulars (de-stressed)",
"noun_adj_source": "kaikki.org Italian (Wiktionary extract)",
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
"unimorph_verb_forms": len(_VERBS),
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
"irregular_verb_lemmas": len(_IRREGV),
"participle_lemmas": len(_PART),
"gerund_lemmas": len(_GER),
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
}
if __name__ == "__main__":
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
tests = [
("parlare", "ind", "present", "first", "singular", "parlo"),
("essere", "ind", "present", "third", "singular", "è"),
("avere", "ind", "present", "first", "singular", "ho"),
("mangiare", "ind", "present", "second", "singular", "mangi"),
("finire", "ind", "present", "first", "singular", "finisco"),
("andare", "ind", "present", "third", "plural", "vanno"),
("fare", "ind", "future", "first", "singular", "farò"),
("potere", "sbjv", "present", "third", "singular", "possa"),
("prendere", "ind", "passato_remoto", "first", "singular", "presi"),
("cercare", "ind", "present", "second", "singular", "cerchi"),
("dormire", "ind", "present", "third", "plural", "dormono"),
("credere", "ind", "future", "first", "singular", "crederò"),
]
ok = 0
for lemma, mood, tense, per, num, exp in tests:
got, conf = conjugate(lemma, mood, tense, per, num)
flag = "OK " if got == exp else "XX "
ok += got == exp
print(f" {flag}{lemma:9} {mood}/{tense:14} {per[:3]}.{num[:2]} -> {got:12} ({conf}) exp={exp}")
print(f"verb tests {ok}/{len(tests)}")
print(" gender: casa=", noun_gender("casa"), "problema=", noun_gender("problema"),
"mano=", noun_gender("mano"), "città=", noun_gender("città"),
"cane=", noun_gender("cane"))
print(" plural: uomo->", inflect_noun("uomo", "plural"),
"| uovo->", inflect_noun("uovo", "plural"),
"| città->", inflect_noun("città", "plural"),
"| amico->", inflect_noun("amico", "plural"),
"| casa->", inflect_noun("casa", "plural"))
print(" adj: italiano/f/pl->", inflect_adj("italiano", "f", "plural"),
"| felice/m/pl->", inflect_adj("felice", "m", "plural"),
"| bello/f/sg->", inflect_adj("bello", "f", "singular"))
print(" part: aprire/f/sg->", participle("aprire", "f", "singular"),
"| prendere/m/pl->", participle("prendere", "m", "plural"),
"| andare/f/sg->", participle("andare", "f", "singular"))
print(" ger: fare->", gerund("fare"), "| parlare->", gerund("parlare"))
-666
View File
@@ -1,666 +0,0 @@
# -*- coding: utf-8 -*-
"""morphology_lat_full.py — production-grade Latin morphological generator.
Latin is the FLAGSHIP dead-language realizer. It rides the *architecture* of the
Romance/Italic engine (the same Realization / spec-driven design and the UniMorph
loader pattern from morphology_it_full.py) but with the CASE SYSTEM RESTORED
the feature Romance lost. Latin therefore exercises machinery the modern Romance
siblings never needed: 5 declensions x 6 cases x 2 numbers x 3 genders, plus a
4-conjugation verb system with tense/mood/voice.
DATA (real, attested no fabrication):
NOUNS + ADJECTIVES UniMorph Latin (github.com/unimorph/lat, CC-BY-SA 3.0)
163,182 N forms across ~thousands of lemmas, each with the full case paradigm
N;NOM/GEN/DAT/ACC/ABL/VOC;SG/PL (real inflected forms, WITH macrons:
puella->puellam, rēx->rēgis, corpus->corporis).
244,197 ADJ forms with case x GENDER x number, incl. UniMorph's combined
tags (GEN+DAT, MASC+FEM, MASC+FEM+NEUT) which are split on load.
462,668 V.PTCP forms (participles) also carry case/gender/number.
UniMorph N tags DO NOT encode inherent gender, so noun gender is inferred
from the declension (nom-sg + gen-sg endings) with a curated exceptions
map the standard, attestable rule (1st decl -a/-ae = fem, 2nd -us/-i =
masc, -um = neut, ...).
VERBS RULE ENGINE (honest gap: UniMorph Latin's verb list is a 947-lemma
sample of rare/prefixed verbs that MISSES every core textbook verb amō,
videō, sum, regō, ... are all absent). Latin conjugation is, however, highly
regular, so verbs are generated by a deterministic 4-conjugation engine over
curated principal parts (present / perfect / supine stems), sourced from
standard references. Irregulars (sum, possum, , ferō, volō, nōlō, mālō)
are curated full tables. Forms are flagged "rule" (not "lexicon") for honesty.
Confidence flag on every form (same contract as the Romance engine):
"lexicon" from UniMorph (trust: high)
"rule" deterministic morphology rule (trust: medium)
"fallback" could not inflect; returned lemma (trust: low -> FLAG)
Public API (used by realizer_lat.py):
decline_noun(lemma, case, number) -> (form, conf)
noun_gender(lemma) -> "m"|"f"|"n"
decline_adj(lemma, case, gender, number) -> (form, conf)
conjugate(lemma, tense, mood, voice, person, number) -> (form, conf)
participle(lemma, kind, case, gender, number) -> (form, conf) # kind: prs|pfv|fut
infinitive(lemma, tense="present", voice="active") -> (form, conf)
lexicon_stats() -> dict
"""
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "lat.unimorph")
_CACHE = os.path.join(_HERE, "data", "lat_morph_cache.pkl")
_CASES = ("NOM", "GEN", "DAT", "ACC", "ABL", "VOC")
_CASE_MAP = {"nom": "NOM", "gen": "GEN", "dat": "DAT", "acc": "ACC",
"abl": "ABL", "voc": "VOC"}
_NUM = {"singular": "SG", "plural": "PL"}
_GEN = {"m": "MASC", "f": "FEM", "n": "NEUT"}
# ── UniMorph loader: noun + adjective + participle case paradigms ────────────────
def _build_cache():
nouns = {} # lemma -> {(CASE, NUM): form}
adjs = {} # lemma -> {(CASE, GEN, NUM): form}
ptcps = {} # lemma -> {(CASE, GEN, NUM): form} (from V.PTCP; keyed loosely)
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
feats = tag.split(";")
head = feats[0]
fs = set(feats)
case = next((c for c in _CASES if c in fs), None)
# handle combined case tags like GEN+DAT
if case is None:
for f in feats:
if "+" in f and any(c in f.split("+") for c in _CASES):
case = [c for c in _CASES if c in f.split("+")]
break
num = "SG" if "SG" in fs else ("PL" if "PL" in fs else None)
if case is None or num is None:
continue
cases = case if isinstance(case, list) else [case]
if head == "N":
d = nouns.setdefault(lemma, {})
for c in cases:
d.setdefault((c, num), form)
elif head == "ADJ":
# gender may be combined: MASC+FEM+NEUT, MASC+FEM
genders = []
for g in ("MASC", "FEM", "NEUT"):
if any(g == x or (g in x.split("+")) for x in feats):
genders.append(g)
if not genders:
genders = ["MASC", "FEM", "NEUT"]
d = adjs.setdefault(lemma, {})
for c in cases:
for g in genders:
d.setdefault((c, g, num), form)
data = {"nouns": nouns, "adjs": adjs, "ptcps": ptcps}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE) and os.path.exists(_UNIMORPH):
if os.path.getmtime(_CACHE) >= os.path.getmtime(_UNIMORPH):
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_NOUNS, _ADJS = _LEX["nouns"], _LEX["adjs"]
# ── noun gender inference (declension-based, curated exceptions) ─────────────────
# Real, attestable rule: gender follows declension + nominative shape, with the
# standard closed set of exceptions.
_GENDER_EXC = {
# 1st-declension masculines (people/agents)
"agricola": "m", "poēta": "m", "nauta": "m", "incola": "m", "scrība": "m",
"auriga": "m", "pīrāta": "m", "athlēta": "m",
# 2nd-declension neuters / feminines
"vīrus": "n", "vulgus": "n", "pelagus": "n", "humus": "f",
# common 3rd-declension whose gender the ending would mispredict
"rēx": "m", "dux": "m", "mīles": "m", "pater": "m", "frāter": "m",
"homō": "m", "leō": "m", "sōl": "m", "mōns": "m", "pōns": "m", "fōns": "m",
"sanguis": "m", "ōrdō": "m", "sermō": "m", "amor": "m", "dolor": "m",
"labor": "m", "timor": "m", "honor": "m", "color": "m", "pēs": "m",
"dēns": "m", "flōs": "m", "mōs": "m", "mensis": "m", "orbis": "m",
"piscis": "m", "ignis": "m", "collis": "m", "grex": "m", "prīnceps": "m",
"māter": "f", "soror": "f", "uxor": "f", "mulier": "f", "virgō": "f",
"urbs": "f", "arx": "f", "pāx": "f", "lēx": "f", "lūx": "f", "vōx": "f",
"nox": "f", "nix": "f", "vīs": "f", "salūs": "f", "virtūs": "f",
"aetās": "f", "cīvitās": "f", "lībertās": "f", "vēritās": "f", "voluptās": "f",
"nātiō": "f", "ratiō": "f", "ōrātiō": "f", "legiō": "f", "regiō": "f",
"mens": "f", "gens": "f", "ars": "f", "pars": "f", "mors": "f", "sors": "f",
"nāvis": "f", "turris": "f", "avis": "f", "vallis": "f", "classis": "f",
"corpus": "n", "tempus": "n", "opus": "n", "genus": "n", "onus": "n",
"pectus": "n", "latus": "n", "vulnus": "n", "scelus": "n", "sīdus": "n",
"caput": "n", "iter": "n", "flūmen": "n", "nōmen": "n", "carmen": "n",
"agmen": "n", "certāmen": "n", "lūmen": "n", "ōmen": "n", "cōgnōmen": "n",
"mare": "n", "animal": "n", "exemplar": "n", "rēte": "n",
# 4th-declension exceptions
"manus": "f", "domus": "f", "tribus": "f", "porticus": "f", "īdūs": "f",
"cornū": "n", "genū": "n", "gelū": "n", "verū": "n",
# 5th-declension
"diēs": "m", "merīdiēs": "m",
}
def _infer_gender(lemma):
if lemma in _GENDER_EXC:
return _GENDER_EXC[lemma]
d = _NOUNS.get(lemma)
nom = d.get(("NOM", "SG")) if d else lemma
gen = d.get(("GEN", "SG")) if d else None
nom = nom or lemma
# 5th declension: gen -eī / -ēī
if gen and (gen.endswith("") or gen.endswith("ēī")):
return "f"
# 1st declension: nom -a, gen -ae
if nom.endswith("a") and (not gen or gen.endswith("ae")):
return "f"
# 2nd declension neuter: nom -um
if nom.endswith("um"):
return "n"
# 2nd declension masc: nom -us/-er/-ir, gen -ī
if (nom.endswith("us") or nom.endswith("er") or nom.endswith("ir")) and \
(not gen or gen.endswith("ī")):
return "m"
# 4th declension: gen -ūs
if gen and gen.endswith("ūs"):
return "n" if nom.endswith("ū") else "m"
# 3rd declension neuters by common nom endings
if nom.endswith(("men", "us", "ur", "al", "ar", "e", "ma")):
# -us here is 3rd-decl neuter type (corpus) only if gen shows -oris/-eris
if nom.endswith("us") and gen and (gen.endswith("oris") or gen.endswith("eris")
or gen.endswith("uris")):
return "n"
if nom.endswith(("men", "al", "ar", "e")):
return "n"
# default 3rd-declension: masculine (most common)
return "m"
_GENDER_CACHE = {}
def noun_gender(lemma):
lemma = lemma.strip()
if lemma not in _GENDER_CACHE:
_GENDER_CACHE[lemma] = _infer_gender(lemma)
return _GENDER_CACHE[lemma]
# ── PUBLIC: noun declension ─────────────────────────────────────────────────────
def decline_noun(lemma, case, number):
lemma = lemma.strip()
C = _CASE_MAP.get(case, case.upper())
N = _NUM.get(number, number)
d = _NOUNS.get(lemma)
if d and (C, N) in d:
return d[(C, N)], "lexicon"
# abl sg often == the -e/-o form; try nom fallback
if d:
# try VOC==NOM, ACC neuter==NOM etc are already in data; last resort lemma
return lemma, "fallback"
return lemma, "fallback"
# ── PUBLIC: adjective declension ────────────────────────────────────────────────
def decline_adj(lemma, case, gender, number):
lemma = lemma.strip()
C = _CASE_MAP.get(case, case.upper())
G = _GEN.get(gender, gender.upper())
N = _NUM.get(number, number)
d = _ADJS.get(lemma)
if d and (C, G, N) in d:
return d[(C, G, N)], "lexicon"
# try other gender (some adjs listed only under MASC+FEM etc handled at load)
if d:
for altG in ("MASC", "FEM", "NEUT"):
if (C, altG, N) in d:
return d[(C, altG, N)], "lexicon"
return lemma, "fallback"
return lemma, "fallback"
# ═══════════════════════════════════════════════════════════════════════════════
# VERB RULE ENGINE (4 conjugations + curated irregulars)
# ═══════════════════════════════════════════════════════════════════════════════
# Curated principal parts for common attested verbs:
# lemma -> (conj, present_stem, perfect_stem, supine_stem)
# conj in {1,2,3,"3io",4}. Stems carry macrons (matching UniMorph orthography).
_VERBS = {
"amō": (1, "am", "amāv", "amāt"),
"laudō": (1, "laud", "laudāv", "laudāt"),
"portō": (1, "port", "portāv", "portāt"),
"vocō": (1, "voc", "vocāv", "vocāt"),
"": (1, "d", "ded", "dat"),
"spectō": (1, "spect", "spectāv", "spectāt"),
"pugnō": (1, "pugn", "pugnāv", "pugnāt"),
"labōrō": (1, "labōr", "labōrāv", "labōrāt"),
"necō": (1, "nec", "necāv", "necāt"),
"parō": (1, "par", "parāv", "parāt"),
"cōgitō": (1, "cōgit", "cōgitāv", "cōgitāt"),
"habitō": (1, "habit", "habitāv", "habitāt"),
"nārrō": (1, "nārr", "nārrāv", "nārrāt"),
"servō": (1, "serv", "servāv", "servāt"),
"superō": (1, "super", "superāv", "superāt"),
"oppugnō": (1, "oppugn", "oppugnāv", "oppugnāt"),
"ambulō": (1, "ambul", "ambulāv", "ambulāt"),
"clāmō": (1, "clām", "clāmāv", "clāmāt"),
"vulnerō": (1, "vulner", "vulnerāv", "vulnerāt"),
"aedificō": (1, "aedific", "aedificāv", "aedificāt"),
"expugnō": (1, "expugn", "expugnāv", "expugnāt"),
"dēfendō": (3, "dēfend", "dēfend", "dēfēns"),
"petō": (3, "pet", "petīv", "petīt"),
"occīdō": (3, "occīd", "occīd", "occīs"),
"interficiō": ("3io", "interfic", "interfēc", "interfect"),
"timeō": (2, "tim", "timu", None),
"iaceō": (2, "iac", "iacu", None),
"pāreō": (2, "pār", "pāru", "pārit"),
"respondeō": (2, "respond", "respond", "respōns"),
"vertō": (3, "vert", "vert", "vers"),
"ostendō": (3, "ostend", "ostend", "ostent"),
"cōnstituō": (3, "cōnstitu", "cōnstitu", "cōnstitūt"),
"cōgnōscō": (3, "cōgnōsc", "cōgnōv", "cōgnit"),
"crēdō": (3, "crēd", "crēdid", "crēdit"),
"ēdūcō": (3, "ēdūc", "ēdūx", "ēduct"),
"cōnservō": (1, "cōnserv", "cōnservāv", "cōnservāt"),
"iuvō": (1, "iuv", "iūv", "iūt"),
"dēbeō": (2, "dēb", "dēbu", "dēbit"),
"moneō": (2, "mon", "monu", "monit"),
"videō": (2, "vid", "vīd", "vīs"),
"habeō": (2, "hab", "habu", "habit"),
"teneō": (2, "ten", "tenu", "tent"),
"timeō": (2, "tim", "timu", None),
"terreō": (2, "terr", "terru", "territ"),
"dēleō": (2, "dēl", "dēlēv", "dēlēt"),
"iubeō": (2, "iub", "iuss", "iuss"),
"maneō": (2, "man", "māns", "māns"),
"moveō": (2, "mov", "mōv", "mōt"),
"doceō": (2, "doc", "docu", "doct"),
"sedeō": (2, "sed", "sēd", "sess"),
"rīdeō": (2, "rīd", "rīs", "rīs"),
"regō": (3, "reg", "rēx", "rēct"),
"dūcō": (3, "dūc", "dūx", "duct"),
"scrībō": (3, "scrīb", "scrīps", "scrīpt"),
"mittō": (3, "mitt", "mīs", "miss"),
"pōnō": (3, "pōn", "posu", "posit"),
"agō": (3, "ag", "ēg", "āct"),
"dīcō": (3, "dīc", "dīx", "dict"),
"gerō": (3, "ger", "gess", "gest"),
"vincō": (3, "vinc", "vīc", "vict"),
"petō": (3, "pet", "petīv", "petīt"),
"legō": (3, "leg", "lēg", "lēct"),
"currō": (3, "curr", "cucurr", "curs"),
"vīvō": (3, "vīv", "vīx", "vīct"),
"quaerō": (3, "quaer", "quaesīv", "quaesīt"),
"trahō": (3, "trah", "trāx", "tract"),
"claudō": (3, "claud", "claus", "claus"),
"cōgō": (3, "cōg", "coēg", "coāct"),
"relinquō": (3, "relinqu", "relīqu", "relict"),
"capiō": ("3io", "cap", "cēp", "capt"),
"faciō": ("3io", "fac", "fēc", "fact"),
"iaciō": ("3io", "iac", "iēc", "iact"),
"rapiō": ("3io", "rap", "rapu", "rapt"),
"fugiō": ("3io", "fug", "fūg", "fugit"),
"cupiō": ("3io", "cup", "cupīv", "cupīt"),
"accipiō": ("3io", "accip", "accēp", "accept"),
"audiō": (4, "aud", "audīv", "audīt"),
"veniō": (4, "ven", "vēn", "vent"),
"sciō": (4, "sc", "scīv", "scīt"),
"sentiō": (4, "sent", "sēns", "sēns"),
"mūniō": (4, "mūn", "mūnīv", "mūnīt"),
"dormiō": (4, "dorm", "dormīv", "dormīt"),
"aperiō": (4, "aper", "aperu", "apert"),
"inveniō": (4, "inven", "invēn", "invent"),
}
# ── Present-system paradigms: full ending tables per conjugation, attached to the
# bare present stem (pstem). Hardcoded from the standard grammar with correct
# macrons/vowel-lengths — deterministic and independently verifiable. Keys:
# (tense, mood, voice) -> {conj: [1sg,2sg,3sg,1pl,2pl,3pl]}
_PARADIGM = {
("present", "ind", "active"): {
1: ["ō", "ās", "at", "āmus", "ātis", "ant"],
2: ["", "ēs", "et", "ēmus", "ētis", "ent"],
3: ["ō", "is", "it", "imus", "itis", "unt"],
"3io": ["", "is", "it", "imus", "itis", "iunt"],
4: ["", "īs", "it", "īmus", "ītis", "iunt"],
},
("present", "ind", "passive"): {
1: ["or", "āris", "ātur", "āmur", "āminī", "antur"],
2: ["eor", "ēris", "ētur", "ēmur", "ēminī", "entur"],
3: ["or", "eris", "itur", "imur", "iminī", "untur"],
"3io": ["ior", "eris", "itur", "imur", "iminī", "iuntur"],
4: ["ior", "īris", "ītur", "īmur", "īminī", "iuntur"],
},
("imperfect", "ind", "active"): {
1: ["ābam", "ābās", "ābat", "ābāmus", "ābātis", "ābant"],
2: ["ēbam", "ēbās", "ēbat", "ēbāmus", "ēbātis", "ēbant"],
3: ["ēbam", "ēbās", "ēbat", "ēbāmus", "ēbātis", "ēbant"],
"3io": ["iēbam", "iēbās", "iēbat", "iēbāmus", "iēbātis", "iēbant"],
4: ["iēbam", "iēbās", "iēbat", "iēbāmus", "iēbātis", "iēbant"],
},
("imperfect", "ind", "passive"): {
1: ["ābar", "ābāris", "ābātur", "ābāmur", "ābāminī", "ābantur"],
2: ["ēbar", "ēbāris", "ēbātur", "ēbāmur", "ēbāminī", "ēbantur"],
3: ["ēbar", "ēbāris", "ēbātur", "ēbāmur", "ēbāminī", "ēbantur"],
"3io": ["iēbar", "iēbāris", "iēbātur", "iēbāmur", "iēbāminī", "iēbantur"],
4: ["iēbar", "iēbāris", "iēbātur", "iēbāmur", "iēbāminī", "iēbantur"],
},
("future", "ind", "active"): {
1: ["ābō", "ābis", "ābit", "ābimus", "ābitis", "ābunt"],
2: ["ēbō", "ēbis", "ēbit", "ēbimus", "ēbitis", "ēbunt"],
3: ["am", "ēs", "et", "ēmus", "ētis", "ent"],
"3io": ["iam", "iēs", "iet", "iēmus", "iētis", "ient"],
4: ["iam", "iēs", "iet", "iēmus", "iētis", "ient"],
},
("future", "ind", "passive"): {
1: ["ābor", "āberis", "ābitur", "ābimur", "ābiminī", "ābuntur"],
2: ["ēbor", "ēberis", "ēbitur", "ēbimur", "ēbiminī", "ēbuntur"],
3: ["ar", "ēris", "ētur", "ēmur", "ēminī", "entur"],
"3io": ["iar", "iēris", "iētur", "iēmur", "iēminī", "ientur"],
4: ["iar", "iēris", "iētur", "iēmur", "iēminī", "ientur"],
},
("present", "sbjv", "active"): {
1: ["em", "ēs", "et", "ēmus", "ētis", "ent"],
2: ["eam", "eās", "eat", "eāmus", "eātis", "eant"],
3: ["am", "ās", "at", "āmus", "ātis", "ant"],
"3io": ["iam", "iās", "iat", "iāmus", "iātis", "iant"],
4: ["iam", "iās", "iat", "iāmus", "iātis", "iant"],
},
("present", "sbjv", "passive"): {
1: ["er", "ēris", "ētur", "ēmur", "ēminī", "entur"],
2: ["ear", "eāris", "eātur", "eāmur", "eāminī", "eantur"],
3: ["ar", "āris", "ātur", "āmur", "āminī", "antur"],
"3io": ["iar", "iāris", "iātur", "iāmur", "iāminī", "iantur"],
4: ["iar", "iāris", "iātur", "iāmur", "iāminī", "iantur"],
},
("imperfect", "sbjv", "active"): {
1: ["ārem", "ārēs", "āret", "ārēmus", "ārētis", "ārent"],
2: ["ērem", "ērēs", "ēret", "ērēmus", "ērētis", "ērent"],
3: ["erem", "erēs", "eret", "erēmus", "erētis", "erent"],
"3io": ["erem", "erēs", "eret", "erēmus", "erētis", "erent"],
4: ["īrem", "īrēs", "īret", "īrēmus", "īrētis", "īrent"],
},
("imperfect", "sbjv", "passive"): {
1: ["ārer", "ārēris", "ārētur", "ārēmur", "ārēminī", "ārentur"],
2: ["ērer", "ērēris", "ērētur", "ērēmur", "ērēminī", "ērentur"],
3: ["erer", "erēris", "erētur", "erēmur", "erēminī", "erentur"],
"3io": ["erer", "erēris", "erētur", "erēmur", "erēminī", "erentur"],
4: ["īrer", "īrēris", "īrētur", "īrēmur", "īrēminī", "īrentur"],
},
}
# perfect-active endings (added to perfect stem) — same for all conjugations
_PERF_ACT = {
("perfect", "ind"): ["ī", "istī", "it", "imus", "istis", "ērunt"],
("pluperfect", "ind"): ["eram", "erās", "erat", "erāmus", "erātis", "erant"],
("futureperfect", "ind"): ["erō", "eris", "erit", "erimus", "eritis", "erint"],
("perfect", "sbjv"): ["erim", "erīs", "erit", "erīmus", "erītis", "erint"],
("pluperfect", "sbjv"):["issem", "issēs", "isset", "issēmus", "issētis", "issent"],
}
def _idx(person, number):
base = {"first": 0, "second": 1, "third": 2}[person]
return base + (0 if number == "singular" else 3)
def _present_system(conj, pstem, tense, mood, voice, person, number):
"""Generate a present-system form (present/imperfect/future ind & subj)."""
table = _PARADIGM.get((tense, mood, voice))
if not table or conj not in table:
return None
return pstem + table[conj][_idx(person, number)]
def _active_infinitive_stem(conj, pstem):
return {1: pstem + "ā", 2: pstem + "ē", 3: pstem + "e",
"3io": pstem + "e", 4: pstem + "ī"}[conj]
_IRREG = {
"sum": {
("present", "ind", "active"): ["sum", "es", "est", "sumus", "estis", "sunt"],
("imperfect", "ind", "active"): ["eram", "erās", "erat", "erāmus", "erātis", "erant"],
("future", "ind", "active"): ["erō", "eris", "erit", "erimus", "eritis", "erunt"],
("perfect", "ind", "active"): ["fuī", "fuistī", "fuit", "fuimus", "fuistis", "fuērunt"],
("pluperfect", "ind", "active"): ["fueram", "fuerās", "fuerat", "fuerāmus", "fuerātis", "fuerant"],
("present", "sbjv", "active"): ["sim", "sīs", "sit", "sīmus", "sītis", "sint"],
("imperfect", "sbjv", "active"): ["essem", "essēs", "esset", "essēmus", "essētis", "essent"],
},
"possum": {
("present", "ind", "active"): ["possum", "potes", "potest", "possumus", "potestis", "possunt"],
("imperfect", "ind", "active"): ["poteram", "poterās", "poterat", "poterāmus", "poterātis", "poterant"],
("future", "ind", "active"): ["poterō", "poteris", "poterit", "poterimus", "poteritis", "poterunt"],
("perfect", "ind", "active"): ["potuī", "potuistī", "potuit", "potuimus", "potuistis", "potuērunt"],
("present", "sbjv", "active"): ["possim", "possīs", "possit", "possīmus", "possītis", "possint"],
},
"": {
("present", "ind", "active"): ["", "īs", "it", "īmus", "ītis", "eunt"],
("imperfect", "ind", "active"): ["ībam", "ībās", "ībat", "ībāmus", "ībātis", "ībant"],
("future", "ind", "active"): ["ībō", "ībis", "ībit", "ībimus", "ībitis", "ībunt"],
("perfect", "ind", "active"): ["", "īstī", "iit", "iimus", "īstis", "iērunt"],
("present", "sbjv", "active"): ["eam", "eās", "eat", "eāmus", "eātis", "eant"],
},
"volō": {
("present", "ind", "active"): ["volō", "vīs", "vult", "volumus", "vultis", "volunt"],
("imperfect", "ind", "active"): ["volēbam", "volēbās", "volēbat", "volēbāmus", "volēbātis", "volēbant"],
("future", "ind", "active"): ["volam", "volēs", "volet", "volēmus", "volētis", "volent"],
("perfect", "ind", "active"): ["voluī", "voluistī", "voluit", "voluimus", "voluistis", "voluērunt"],
("present", "sbjv", "active"): ["velim", "velīs", "velit", "velīmus", "velītis", "velint"],
},
"nōlō": {
("present", "ind", "active"): ["nōlō", "nōn vīs", "nōn vult", "nōlumus", "nōn vultis", "nōlunt"],
("present", "sbjv", "active"): ["nōlim", "nōlīs", "nōlit", "nōlīmus", "nōlītis", "nōlint"],
},
"ferō": {
("present", "ind", "active"): ["ferō", "fers", "fert", "ferimus", "fertis", "ferunt"],
("imperfect", "ind", "active"): ["ferēbam", "ferēbās", "ferēbat", "ferēbāmus", "ferēbātis", "ferēbant"],
("future", "ind", "active"): ["feram", "ferēs", "feret", "ferēmus", "ferētis", "ferent"],
("perfect", "ind", "active"): ["tulī", "tulistī", "tulit", "tulimus", "tulistis", "tulērunt"],
("present", "sbjv", "active"): ["feram", "ferās", "ferat", "ferāmus", "ferātis", "ferant"],
},
}
def conjugate(lemma, tense, mood, voice="active", person="third", number="singular"):
"""Return (surface, confidence). Perfect-passive forms are periphrastic and
handled in the realizer (sum + PPP); this returns synthetic forms only."""
lemma = lemma.strip()
i = _idx(person, number)
ir = _IRREG.get(lemma)
if ir:
tbl = ir.get((tense, mood, voice)) or ir.get((tense, mood, "active"))
if tbl and tbl[i]:
return tbl[i], "rule"
v = _VERBS.get(lemma)
if not v:
v = _infer_principal_parts(lemma)
if not v:
return lemma, "fallback"
conj, pstem, perfstem, supstem = v
# imperative (present active) 2sg / 2pl
if mood == "imp":
return _imperative(conj, pstem, person, number), "rule"
# perfect-system active
if tense in ("perfect", "pluperfect", "futureperfect") and voice == "active":
if not perfstem:
return lemma, "fallback"
end = _PERF_ACT.get((tense, mood))
if end:
return perfstem + end[i], "rule"
# present-system (active + passive)
if tense in ("present", "imperfect", "future"):
form = _present_system(conj, pstem, tense, mood, voice, person, number)
if form:
return form, "rule"
return lemma, "fallback"
def _imperative(conj, pstem, person, number):
if number == "singular":
return {1: pstem + "ā", 2: pstem + "ē", 3: pstem + "e",
"3io": pstem + "e", 4: pstem + "ī"}[conj]
return {1: pstem + "āte", 2: pstem + "ēte", 3: pstem + "ite",
"3io": pstem + "ite", 4: pstem + "īte"}[conj]
def _infer_principal_parts(lemma):
"""OOV fallback: infer conjugation + stems from the 1sg-present citation form.
Perfect/supine stems are guessed regularly (often wrong for 3rd conj) and the
resulting forms are still returned as 'rule' but the realizer down-weights."""
if lemma.endswith("ō"):
base = lemma[:-1]
# can't distinguish conj from 1sg alone reliably; default by ending vowel
if base.endswith("i"):
return ("3io", base[:-1], base[:-1] + "īv", base[:-1] + "īt")
return (3, base, base + "s", base + "t")
return None
# ── PUBLIC: participles ─────────────────────────────────────────────────────────
def participle(lemma, kind, case="nom", gender="m", number="singular"):
"""kind: 'prs' (present active, -ns/-ntis), 'pfv' (perfect passive, -tus),
'fut' (future active, -tūrus). Declined as an adjective via rule endings.
Returns (form, conf)."""
v = _VERBS.get(lemma)
if not v:
return lemma, "fallback"
conj, pstem, perfstem, supstem = v
if kind == "pfv":
if not supstem:
return lemma, "fallback"
base = supstem[:-1] if supstem.endswith("t") or supstem.endswith("s") else supstem
stem = supstem # supine stem already ends in t/s: amāt- -> amātus
return _decline_us_a_um(stem, case, gender, number), "rule"
if kind == "fut":
if not supstem:
return lemma, "fallback"
return _decline_us_a_um(supstem + "ūr", case, gender, number), "rule"
if kind == "prs":
# present active participle: stem + ns (nom), stem + nt- (oblique), 3rd-decl
pv = {1: "ā", 2: "ē", 3: "ē", "3io": "", 4: ""}[conj]
ntstem = pstem + pv + "nt"
return _decline_pres_ptcp(pstem + pv, case, gender, number), "rule"
return lemma, "fallback"
def _decline_us_a_um(stem, case, gender, number):
"""Decline a -us/-a/-um adjective/participle stem (2-1-2 declension)."""
C = _CASE_MAP.get(case, case.upper())
end = {
("NOM", "m", "singular"): "us", ("NOM", "f", "singular"): "a", ("NOM", "n", "singular"): "um",
("GEN", "m", "singular"): "ī", ("GEN", "f", "singular"): "ae", ("GEN", "n", "singular"): "ī",
("DAT", "m", "singular"): "ō", ("DAT", "f", "singular"): "ae", ("DAT", "n", "singular"): "ō",
("ACC", "m", "singular"): "um", ("ACC", "f", "singular"): "am", ("ACC", "n", "singular"): "um",
("ABL", "m", "singular"): "ō", ("ABL", "f", "singular"): "ā", ("ABL", "n", "singular"): "ō",
("VOC", "m", "singular"): "e", ("VOC", "f", "singular"): "a", ("VOC", "n", "singular"): "um",
("NOM", "m", "plural"): "ī", ("NOM", "f", "plural"): "ae", ("NOM", "n", "plural"): "a",
("GEN", "m", "plural"): "ōrum", ("GEN", "f", "plural"): "ārum", ("GEN", "n", "plural"): "ōrum",
("DAT", "m", "plural"): "īs", ("DAT", "f", "plural"): "īs", ("DAT", "n", "plural"): "īs",
("ACC", "m", "plural"): "ōs", ("ACC", "f", "plural"): "ās", ("ACC", "n", "plural"): "a",
("ABL", "m", "plural"): "īs", ("ABL", "f", "plural"): "īs", ("ABL", "n", "plural"): "īs",
("VOC", "m", "plural"): "ī", ("VOC", "f", "plural"): "ae", ("VOC", "n", "plural"): "a",
}.get((C, gender, number), "us")
return stem + end
def _decline_pres_ptcp(stem, case, gender, number):
"""Present active participle (amāns, amantis) — 3rd-declension, stem+ns/nt."""
C = _CASE_MAP.get(case, case.upper())
if C == "NOM" and number == "singular":
return stem + "ns"
if C == "VOC" and number == "singular":
return stem + "ns"
base = stem + "nt"
end = {
("GEN", "singular"): "is", ("DAT", "singular"): "ī",
("ACC", "singular"): "em" if gender != "n" else "",
("ABL", "singular"): "e",
("NOM", "plural"): "ēs" if gender != "n" else "ia",
("GEN", "plural"): "ium", ("DAT", "plural"): "ibus",
("ACC", "plural"): "ēs" if gender != "n" else "ia",
("ABL", "plural"): "ibus", ("VOC", "plural"): "ēs",
}.get((C, number), "is")
if C == "ACC" and number == "singular" and gender == "n":
return stem + "ns"
return base + end
def infinitive(lemma, tense="present", voice="active"):
lemma = lemma.strip()
if lemma == "sum":
return ("esse", "rule") if tense == "present" else ("fuisse", "rule")
v = _VERBS.get(lemma)
if not v:
return lemma, "fallback"
conj, pstem, perfstem, supstem = v
if tense == "present":
if voice == "active":
return _active_infinitive_stem(conj, pstem).rstrip() + \
("re" if conj != 3 and conj != "3io" else "re"), "rule"
# passive present infinitive
base = {1: pstem + "ā", 2: pstem + "ē", 4: pstem + "ī"}.get(conj)
if base:
return base + "", "rule"
return pstem + "ī", "rule" # 3rd: regī
if tense == "perfect" and voice == "active" and perfstem:
return perfstem + "isse", "rule"
return lemma, "fallback"
def lexicon_stats():
return {
"noun_adj_source": "UniMorph Latin (github.com/unimorph/lat, CC-BY-SA 3.0)",
"verb_source": "rule-based 4-conjugation engine over curated attested "
"principal parts (UniMorph verb list is a 947-lemma sample "
"MISSING all core verbs — amō/sum/videō absent)",
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
"curated_verb_lemmas": len(_VERBS) + len(_IRREG),
"gender_inference": "declension-based (nom+gen endings) + curated exceptions",
}
if __name__ == "__main__":
import json
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
print("\n-- noun declension puella (1st, fem) --")
for c in ("nom", "gen", "dat", "acc", "abl", "voc"):
print(f" {c}: sg={decline_noun('puella', c, 'singular')[0]:10} "
f"pl={decline_noun('puella', c, 'plural')[0]}")
print("\n-- rēx (3rd, m):", [decline_noun('rēx', c, 'singular')[0] for c in ('nom','gen','dat','acc','abl')])
print("-- gender: puella=", noun_gender("puella"), "rēx=", noun_gender("rēx"),
"bellum=", noun_gender("bellum"), "corpus=", noun_gender("corpus"),
"manus=", noun_gender("manus"), "diēs=", noun_gender("diēs"))
print("\n-- conjugate videō (2nd) present ind active --")
for p in ("first", "second", "third"):
for n in ("singular", "plural"):
print(f" {p[:3]}.{n[:2]}: {conjugate('videō','present','ind','active',p,n)[0]}")
print("-- amō forms:", conjugate("amō","present","ind","active","first","singular")[0],
conjugate("amō","imperfect","ind","active","third","plural")[0],
conjugate("amō","future","ind","active","first","singular")[0],
conjugate("amō","perfect","ind","active","third","singular")[0])
print("-- sum:", [conjugate("sum","present","ind","active",p,"singular")[0] for p in ("first","second","third")])
print("-- participle amō pfv acc.f.sg:", participle("amō","pfv","acc","f","singular")[0])
print("-- infinitive amō:", infinitive("amō")[0], "| regō pass:", infinitive("regō", voice="passive")[0])
-538
View File
@@ -1,538 +0,0 @@
"""morphology_pt_full.py — production-grade Brazilian-Portuguese morphological generator.
NOT a toy. Backed by two real, broad, Wiktionary-lineage lexicons:
VERBS UniMorph Portuguese (github.com/unimorph/por, CC-BY-SA 3.0)
4,001 verb lemmas × full paradigm (283,991 finite/non-finite forms +
20,005 participle forms). Every mood/tense pt actually inflects:
indicative present / preterite (PST;PFV) / imperfect (PST;IPFV) /
pluperfect-simple (PST;PRF) / future,
conditional (futuro do pretérito),
subjunctive present / imperfect / FUTURE (PT-specific live tense),
affirmative + negative imperative,
PERSONAL infinitive (V;{p};{n};NFIN a PT-specific finite-ish form),
past participle (4 gender/number forms) + gerúndio (V.PTCP;PRS).
NOUNS + ADJECTIVES kaikki.org Portuguese (Wiktionary extract, same lineage)
81,138 noun lemmas WITH inherent gender + real (often irregular) plural
so -ão-ões / -ãos / -ães / -õos is resolved PER LEMMA by Wiktionary,
never guessed (mãomãos, pãopães, coraçãocorações).
40,252 adjective lemmas with real feminine + masc/fem plural forms.
Fallbacks (degrade, never crash, on out-of-vocabulary input):
verbs : rule generator for regular -ar/-er/-ir paradigms
nouns : gender heuristic (endings) + rule pluralization (with -ão FLAGGED)
adjs : -o/-a gender rule + rule pluralization
Confidence flag on every form:
"lexicon" straight from UniMorph/kaikki (trust: high)
"rule" deterministic rule (trust: medium)
"fallback" could not inflect; returned lemma (trust: low -> FLAG)
Public API (used by realizer_pt.py):
conjugate(lemma, mood, tense, person, number) -> (form, conf)
personal_infinitive(lemma, person, number) -> (form, conf)
participle(lemma, gender="m", number="singular") -> (form, conf)
gerund(lemma) -> (form, conf)
noun_gender(lemma) -> "m"|"f"
inflect_noun(lemma, number, gender=None) -> (form, conf)
inflect_adj(lemma, gender, number) -> (form, conf)
lexicon_stats() -> dict
"""
import json
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "por.unimorph")
_KAIKKI = os.path.join(_HERE, "data", "kaikki_pt.jsonl")
_CACHE = os.path.join(_HERE, "data", "pt_morph_cache.pkl")
# ── mood/tense pair -> UniMorph feature triple (a in tag; b in tag; c in tag) ────
_VERB_KEYMAP = {
("ind", "present"): ("IND", "PRS", None),
("ind", "preterite"): ("IND", "PST", "PFV"),
("ind", "imperfect"): ("IND", "PST", "IPFV"),
("ind", "pluperfect"): ("IND", "PST", "PRF"), # simple mais-que-perfeito
("ind", "future"): ("IND", "FUT", None),
("ind", "conditional"): ("COND", None, None),
("sbjv", "present"): ("SBJV", "PRS", None),
("sbjv", "imperfect"): ("SBJV", "PST", "IPFV"),
("sbjv", "future"): ("SBJV", "FUT", None), # PT-specific
("imp", "affirmative"): ("IMP", "POS", None),
("imp", "negative"): ("IMP", "NEG", None),
}
_PERSON = {"first": "1", "second": "2", "third": "3"}
_NUMBER = {"singular": "SG", "plural": "PL"}
def _feat_set(tag):
return set(tag.split(";"))
# ── build the compact lexicon from UniMorph (verbs) + kaikki (nouns/adjs) ────────
def _build_verbs():
verbs = {} # (lemma, "mood|tense|person|number") -> form
pinf = {} # (lemma, "person|number") -> personal-infinitive form
part = {} # lemma -> {("m","SG"): form, ...} past participle
ger = {} # lemma -> gerúndio
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
f = _feat_set(tag)
head = tag.split(";")[0]
if head == "V.PTCP":
if "PST" in f: # past participle: falado/falada/falados/faladas
g = "m" if "MASC" in f else ("f" if "FEM" in f else "m")
num = "SG" if "SG" in f else ("PL" if "PL" in f else "SG")
part.setdefault(lemma, {})[(g, num)] = form
elif "PRS" in f: # gerúndio: falando
ger.setdefault(lemma, form)
continue
if head != "V":
continue
# personal / impersonal infinitive
if "NFIN" in f:
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person and number:
pinf[(lemma, f"{person}|{number}")] = form
continue
# finite forms
mt = None
for (mood, tense), (a, b, c) in _VERB_KEYMAP.items():
if a not in f:
continue
if b is not None and b not in f:
continue
if c is not None and c not in f:
continue
# IND;PST needs exactly PFV|IPFV|PRF — reject if the required one absent
mt = (mood, tense)
break
if mt is None:
continue
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person is None or number is None:
continue
verbs.setdefault((lemma, f"{mt[0]}|{mt[1]}|{person}|{number}"), form)
return verbs, pinf, part, ger
def _kaikki_gender(arg):
if not arg:
return None
a = arg.lower()
if a.startswith("f"):
return "f"
if a.startswith("m"):
return "m"
return None
def _build_nouns_adjs():
nouns = {} # lemma -> {"g","SG","PL"}
adjs = {} # lemma -> {("m","SG"),("f","SG"),("m","PL"),("f","PL")}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
pos = d.get("pos")
word = d.get("word", "")
if not word or " " in word: # skip multiword entries
continue
forms = d.get("forms", []) or []
if pos == "noun":
ht = d.get("head_templates") or []
g = None
if ht:
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
if g is None:
tags = d.get("tags") or []
if "feminine" in tags:
g = "f"
elif "masculine" in tags:
g = "m"
pl = None
for x in forms:
t = x.get("tags") or []
if "plural" in t and "alternative" not in t and "obsolete" not in t:
pl = x.get("form")
break
# first entry wins; but a later entry with a plural fills a gap
if word not in nouns:
nouns[word] = {"g": g, "SG": word, "PL": pl}
else:
cur = nouns[word]
if cur.get("g") is None and g:
cur["g"] = g
if not cur.get("PL") and pl:
cur["PL"] = pl
elif pos == "adj":
d0 = adjs.setdefault(word, {})
d0.setdefault(("m", "SG"), word)
for x in forms:
t = set(x.get("tags") or [])
fm = x.get("form")
if not fm or ("alternative" in t) or ("obsolete" in t):
continue
if "comparative" in t or "superlative" in t or \
"diminutive" in t or "augmentative" in t:
continue
if "feminine" in t and "plural" in t:
d0[("f", "PL")] = fm
elif "masculine" in t and "plural" in t:
d0[("m", "PL")] = fm
elif "feminine" in t:
d0[("f", "SG")] = fm
elif "plural" in t: # invariant-gender adj (feliz -> felizes)
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
return nouns, adjs
def _build_cache():
verbs, pinf, part, ger = _build_verbs()
nouns, adjs = _build_nouns_adjs()
data = {"verbs": verbs, "pinf": pinf, "part": part, "ger": ger,
"nouns": nouns, "adjs": adjs}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE):
newest_src = max(os.path.getmtime(_UNIMORPH),
os.path.getmtime(_KAIKKI) if os.path.exists(_KAIKKI) else 0)
if os.path.getmtime(_CACHE) >= newest_src:
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_VERBS, _PINF, _PART, _GER, _NOUNS, _ADJS = (
_LEX["verbs"], _LEX["pinf"], _LEX["part"], _LEX["ger"],
_LEX["nouns"], _LEX["adjs"])
# ── regular-ending rule fallback (deterministic, last resort) ────────────────────
def _vclass(lemma):
return lemma[-2:] if lemma[-2:] in ("ar", "er", "ir") else None
def _stem(lemma):
return lemma[:-2]
# endings indexed [1sg,2sg,3sg,1pl,2pl,3pl]
_REG = {
("ind", "present", "ar"): ["o", "as", "a", "amos", "ais", "am"],
("ind", "present", "er"): ["o", "es", "e", "emos", "eis", "em"],
("ind", "present", "ir"): ["o", "es", "e", "imos", "is", "em"],
("ind", "preterite", "ar"): ["ei", "aste", "ou", "amos", "astes", "aram"],
("ind", "preterite", "er"): ["i", "este", "eu", "emos", "estes", "eram"],
("ind", "preterite", "ir"): ["i", "iste", "iu", "imos", "istes", "iram"],
("ind", "imperfect", "ar"): ["ava", "avas", "ava", "ávamos", "áveis", "avam"],
("ind", "imperfect", "er"): ["ia", "ias", "ia", "íamos", "íeis", "iam"],
("ind", "imperfect", "ir"): ["ia", "ias", "ia", "íamos", "íeis", "iam"],
("sbjv", "present", "ar"): ["e", "es", "e", "emos", "eis", "em"],
("sbjv", "present", "er"): ["a", "as", "a", "amos", "ais", "am"],
("sbjv", "present", "ir"): ["a", "as", "a", "amos", "ais", "am"],
("sbjv", "imperfect", "ar"): ["asse", "asses", "asse", "ássemos", "ásseis", "assem"],
("sbjv", "imperfect", "er"): ["esse", "esses", "esse", "êssemos", "êsseis", "essem"],
("sbjv", "imperfect", "ir"): ["isse", "isses", "isse", "íssemos", "ísseis", "issem"],
("sbjv", "future", "ar"): ["ar", "ares", "ar", "armos", "ardes", "arem"],
("sbjv", "future", "er"): ["er", "eres", "er", "ermos", "erdes", "erem"],
("sbjv", "future", "ir"): ["ir", "ires", "ir", "irmos", "irdes", "irem"],
}
# future & conditional attach to the FULL infinitive
_FUT = ["ei", "ás", "á", "emos", "eis", "ão"]
_COND = ["ia", "ias", "ia", "íamos", "íeis", "iam"]
def _slot_idx(person, number):
base = {"first": 0, "second": 1, "third": 2}[person]
return base + (0 if number == "singular" else 3)
def _rule_conjugate(lemma, mood, tense, person, number):
vc = _vclass(lemma)
if vc is None:
return None
st, i = _stem(lemma), _slot_idx(person, number)
if mood == "ind" and tense == "future":
return lemma + _FUT[i]
if mood == "ind" and tense == "conditional":
return lemma + _COND[i]
if mood == "imp": # affirmative tú/vocês imperative ~ subjunctive present
table = _REG.get(("sbjv", "present", vc))
if table and tense == "negative":
return st + table[i]
# affirmative 2sg = 3sg present indicative; others = subjunctive
pres = _REG.get(("ind", "present", vc))
if person == "second" and number == "singular":
return st + pres[2]
return st + table[i] if table else None
table = _REG.get((mood, tense, vc))
if table:
return st + table[i]
return None
# verified corrections to UniMorph data errors (each audited individually, not
# guessed). The three 1PL-present entries are glued-allomorph errors surfaced by a
# full-lexicon scan for a non-final "mos" in V;1;PL;IND;PRS forms (the ONLY three).
_VERB_FIX = {
("estar", "ind", "imperfect", "third", "plural"): "estavam", # was "estávam"
("estar", "ind", "present", "first", "plural"): "estamos", # was "estamosestámos"
("haver", "ind", "present", "first", "plural"): "havemos", # was "havemoshemos"
("ir", "ind", "present", "first", "plural"): "vamos", # was "vamosimos"
}
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
def conjugate(lemma, mood, tense, person, number):
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
lemma = lemma.strip().lower()
fix = _VERB_FIX.get((lemma, mood, tense, person, number))
if fix:
return fix, "lexicon"
p, n = _PERSON.get(person), _NUMBER.get(number)
if p and n:
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
if form:
# pt-BR normalization: UniMorph `por` carries the EUROPEAN spelling of
# the -ar 1pl PRETERITE (-ámos). Brazilian PT drops the accent
# (falámos->falamos, chegámos->chegamos) — 3,334/4,001 verbs affected.
if (mood == "ind" and tense == "preterite" and person == "first"
and number == "plural" and form.endswith("ámos")):
form = form[:-4] + "amos"
return form, "lexicon"
r = _rule_conjugate(lemma, mood, tense, person, number)
if r:
return r, "rule"
return lemma, "fallback"
def personal_infinitive(lemma, person, number):
"""PT personal (inflected) infinitive: para falarmos, ao chegarem."""
lemma = lemma.strip().lower()
p, n = _PERSON.get(person), _NUMBER.get(number)
if p and n:
form = _PINF.get((lemma, f"{p}|{n}"))
if form:
return form, "lexicon"
# rule: infinitive + personal endings (-, -es, -, -mos, -des, -em)
end = {("first", "singular"): "", ("second", "singular"): "es",
("third", "singular"): "", ("first", "plural"): "mos",
("second", "plural"): "des", ("third", "plural"): "em"}.get((person, number), "")
return lemma + end, "rule"
# ── PUBLIC: participle + gerund ───────────────────────────────────────────────────
def participle(lemma, gender="m", number="singular"):
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
d = _PART.get(lemma)
if d:
form = d.get((g, num)) or d.get(("m", "SG"))
if form:
return form, "lexicon"
if lemma.endswith("ar"):
base = lemma[:-2] + "ad"
elif lemma[-2:] in ("er", "ir"):
base = lemma[:-2] + "id"
else:
return lemma, "fallback"
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "os", "f|PL": "as"}[f"{g}|{num}"]
return base + suf, "rule"
def gerund(lemma):
lemma = lemma.strip().lower()
if lemma in _GER:
return _GER[lemma], "lexicon"
if lemma.endswith("ar"):
return lemma[:-2] + "ando", "rule"
if lemma.endswith("er"):
return lemma[:-2] + "endo", "rule"
if lemma.endswith("ir"):
return lemma[:-2] + "indo", "rule"
return lemma, "fallback"
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
_FEM_SUF = ("ção", "são", "ção", "dade", "tade", "agem", "igem", "ugem", "gem",
"ez", "eza", "ice", "ície", "tude", "ude", "âncbefore")
_FEM_SUF = ("ção", "são", "dade", "tade", "agem", "gem", "eza", "ez", "ice",
"tude", "ude", "ância", "ência", "ínia")
_MASC_SUF = ("ema", "oma", "ama", "grama", "eta", "ão") # Greek -ma etc. (mostly m)
def _gender_heuristic(noun):
for suf in _FEM_SUF:
if noun.endswith(suf):
return "f"
if noun.endswith(("ema", "oma", "ama")): # problema, idioma, programa
return "m"
if noun.endswith("a") or noun.endswith("ã"):
return "f"
if noun.endswith("o") or noun.endswith(("l", "r", "z", "m", "u", "i")):
return "m"
return "m"
def noun_gender(lemma):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if d and d.get("g"):
return d["g"]
return _gender_heuristic(lemma)
_INVARIANT_PL_SUF = ("s",) # paroxytones ending -s are invariant (o lápis / os lápis)
def _rule_plural(noun):
"""Deterministic PT pluralization. Returns (form, ok) where ok=False flags an
ambiguous -ão that should lower confidence (the lexicon normally resolves it)."""
if not noun:
return noun, True
if noun.endswith("ão"):
return noun[:-2] + "ões", False # majority rule, but AMBIGUOUS -> flag
if noun.endswith("m"):
return noun[:-1] + "ns", True # homem->homens, jardim->jardins
if noun.endswith("al"):
return noun[:-2] + "ais", True
if noun.endswith("el"):
return noun[:-2] + "éis", True
if noun.endswith("ol"):
return noun[:-2] + "óis", True
if noun.endswith("ul"):
return noun[:-2] + "uis", True
if noun.endswith("il"):
return noun[:-2] + "is", True # stressed (funil->funis); unstressed rarer
if noun.endswith(("r", "z")):
return noun + "es", True # flor->flores, luz->luzes
if noun.endswith("s"):
# paroxytone -s (lápis, ônibus) invariant; oxytone -s (país) -> -es
return noun, True
if noun.endswith(("a", "e", "i", "o", "u", "á", "é", "í", "ó", "ú", "ã")):
return noun + "s", True
return noun + "s", True
def inflect_noun(lemma, number, gender=None):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if number == "singular":
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
if d and d.get("PL"):
return d["PL"], "lexicon"
form, ok = _rule_plural(lemma)
return form, ("rule" if ok else "fallback")
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
def inflect_adj(lemma, gender, number):
lemma = lemma.strip().lower()
g = "f" if gender == "f" else "m"
num = "SG" if number == "singular" else "PL"
d = _ADJS.get(lemma)
if d:
form = d.get((g, num))
if form:
return form, "lexicon"
# build a missing plural from this gender's singular
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
if num == "PL":
pl, ok = _rule_plural(sg)
return pl, ("rule" if ok else "fallback")
return sg, "lexicon"
# rule fallback: -o/-a gender, then pluralize
a = lemma
if g == "f":
if a.endswith("o"):
a = a[:-1] + "a"
elif a.endswith(("ês", "or")) and not a.endswith("ior"):
a = a + "a" # português->portuguesa, trabalhador->..a
if num == "PL":
a, ok = _rule_plural(a)
return a, ("rule" if ok else "fallback")
return a, "rule"
def lexicon_stats():
return {
"verb_source": "UniMorph Portuguese (github.com/unimorph/por)",
"noun_adj_source": "kaikki.org Portuguese (Wiktionary extract)",
"license": "CC-BY-SA (Wiktionary-derived)",
"verb_forms": len(_VERBS),
"verb_lemmas": len({k[0] for k in _VERBS}),
"personal_infinitive_forms": len(_PINF),
"participle_lemmas": len(_PART),
"gerund_lemmas": len(_GER),
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
}
if __name__ == "__main__":
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
tests = [
("falar", "ind", "present", "first", "singular", "falo"),
("comer", "ind", "present", "third", "plural", "comem"),
("partir", "ind", "present", "first", "plural", "partimos"),
("ser", "ind", "present", "third", "singular", "é"),
("ir", "ind", "preterite", "first", "singular", "fui"),
("ter", "ind", "future", "first", "singular", "terei"),
("fazer", "sbjv", "present", "first", "singular", "faça"),
("dormir", "ind", "present", "first", "singular", "durmo"),
("dar", "ind", "preterite", "third", "singular", "deu"),
("poder", "ind", "conditional", "first", "singular", "poderia"),
("fazer", "sbjv", "future", "third", "singular", "fizer"),
("estar", "ind", "present", "third", "singular", "está"),
]
ok = 0
for lemma, mood, tense, per, num, exp in tests:
got, conf = conjugate(lemma, mood, tense, per, num)
flag = "OK " if got == exp else "XX "
ok += got == exp
print(f" {flag}{lemma:8} {mood}/{tense} {per[:3]}.{num[:2]} -> {got:14} ({conf}) exp={exp}")
print(f"verb tests {ok}/{len(tests)}")
print(" gender: casa=", noun_gender("casa"), "problema=", noun_gender("problema"),
"mão=", noun_gender("mão"), "coração=", noun_gender("coração"),
"flor=", noun_gender("flor"))
print(" plural: mão->", inflect_noun("mão", "plural"),
"| pão->", inflect_noun("pão", "plural"),
"| animal->", inflect_noun("animal", "plural"),
"| coração->", inflect_noun("coração", "plural"))
print(" adj: bonito/f/sg->", inflect_adj("bonito", "f", "singular"),
"| feliz/m/pl->", inflect_adj("feliz", "m", "plural"),
"| português/f/sg->", inflect_adj("português", "f", "singular"))
print(" part: fazer/m/sg->", participle("fazer"), "| ger falar->", gerund("falar"))
print(" pinf falar 1pl->", personal_infinitive("falar", "first", "plural"))
-609
View File
@@ -1,609 +0,0 @@
# -*- coding: utf-8 -*-
"""morphology_ro_full.py — production-grade Romanian morphological generator.
Romanian is the BIG typological delta of the Romance family. The verb engine and
the confidence/fallback contract TRANSFER from the Italian sibling; the NOMINAL
system is genuinely new: Romanian has a SUFFIXED definite article, a preserved
NOM/ACC vs GEN/DAT case distinction, a NEUTER gender (masc-agreeing in SG,
fem-agreeing in PL), and a VOCATIVE. Those are grounded in real per-lemma data,
not guessed.
Real, Wiktionary-lineage lexical sources:
VERBS UniMorph Romanian (github.com/unimorph/ron, CC-BY-SA 3.0)
~1216 verb lemmas × paradigm, CLEAN orthography:
indicativ prezent / imperfect (PST;IPFV) / perfectul simplu (PST;PFV) /
conjunctiv prezent (SBJV;PRS, stored WITHOUT the '' particle),
participiu (V.PTCP;PST, INVARIABLE in the perfect compus),
gerunziu (V.CVB;PRS), infinitiv (NFIN), imperativ.
ro_irreg_verbs (embedded) high-frequency verbs UniMorph MISSES
(avea, vrea, da) + the auxiliary clitic paradigms the compound tenses need
(perfect-compus am/ai/a/am/ați/au, viitor voi/vei/va/vom/veți/vor,
condițional /ai/ar/am/ați/ar). Real standard forms.
NOUNS kaikki.org Romanian (Wiktionary extract, CC-BY-SA 3.0)
the FULL declension per lemma, cleanly tagged:
(nom/acc | gen/dat | vocative) × (indefinite | definite) × (sg | pl).
This is what makes the suffixed article LEXICALLY grounded (omomul,
casăcasa, băiatbăiatul, casei gen/dat, omule vocative). Inherent gender
m / f / n (NEUTER available directly) from the head template.
ADJECTIVES UniMorph Romanian ADJ
full case × gender(MASC/FEM/NEUT) × number × definiteness paradigm.
Fallbacks (degrade, never crash, on OOV): rule verb conjugation for -a/-ea/-e/-i/-î
classes, rule pluralization, rule suffixed-article by gender+ending. Every form
carries a confidence flag: "lexicon" | "rule" | "fallback".
Public API (used by realizer_ro.py):
conjugate(lemma, mood, tense, person, number) -> (form, conf)
aux(kind, person, number) -> str # perfect / future / conditional clitics
participle(lemma) -> (form, conf) # INVARIABLE
gerund(lemma) -> (form, conf)
noun_gender(lemma) -> "m"|"f"|"n"
definite_suffix(noun, gender, number, case) -> (form, conf) # rule engine
inflect_noun(lemma, number, gender=None, case="nomacc", definite=False) -> (form, conf)
inflect_adj(lemma, gender, number, case="nomacc", definite=False) -> (form, conf)
lexicon_stats() -> dict
"""
import json
import os
import pickle
_HERE = os.path.dirname(os.path.abspath(__file__))
_UNIMORPH = os.path.join(_HERE, "data", "ron.unimorph")
_KAIKKI = os.path.join(_HERE, "data", "kaikki_ro.jsonl")
_CACHE = os.path.join(_HERE, "data", "ro_morph_cache.pkl")
# ── (mood, tense) -> UniMorph feature set ─────────────────────────────────────────
_VERB_KEYMAP = {
("ind", "present"): {"IND", "PRS"},
("ind", "imperfect"): {"IND", "PST", "IPFV"},
("ind", "perfect_s"): {"IND", "PST", "PFV"}, # perfectul simplu (regional/lit.)
("sbjv", "present"): {"SBJV", "PRS"},
("imp", "affirmative"): {"POS", "IMP"},
}
_PERSON = {"first": "1", "second": "2", "third": "3"}
_NUMBER = {"singular": "SG", "plural": "PL"}
def _feat_set(tag):
return set(tag.split(";"))
# ── high-frequency irregulars UniMorph misses + auxiliary clitic paradigms ────────
# Real standard Romanian forms (textbook paradigms).
_IRREG = {
"avea": {
"ind|present|1|SG": "am", "ind|present|2|SG": "ai", "ind|present|3|SG": "are",
"ind|present|1|PL": "avem", "ind|present|2|PL": "aveți", "ind|present|3|PL": "au",
"ind|imperfect|1|SG": "aveam", "ind|imperfect|2|SG": "aveai",
"ind|imperfect|3|SG": "avea", "ind|imperfect|1|PL": "aveam",
"ind|imperfect|2|PL": "aveați", "ind|imperfect|3|PL": "aveau",
"sbjv|present|3|SG": "aibă", "sbjv|present|3|PL": "aibă",
"sbjv|present|1|SG": "am", "sbjv|present|2|SG": "ai",
"sbjv|present|1|PL": "avem", "sbjv|present|2|PL": "aveți",
"part": "avut", "ger": "având",
},
"vrea": {
"ind|present|1|SG": "vreau", "ind|present|2|SG": "vrei", "ind|present|3|SG": "vrea",
"ind|present|1|PL": "vrem", "ind|present|2|PL": "vreți", "ind|present|3|PL": "vor",
"ind|imperfect|1|SG": "voiam", "ind|imperfect|3|SG": "voia",
"sbjv|present|3|SG": "vrea", "sbjv|present|3|PL": "vrea",
"part": "vrut", "ger": "vrând",
},
"da": {
"ind|present|1|SG": "dau", "ind|present|2|SG": "dai", "ind|present|3|SG": "",
"ind|present|1|PL": "dăm", "ind|present|2|PL": "dați", "ind|present|3|PL": "dau",
"ind|imperfect|1|SG": "dădeam", "ind|imperfect|3|SG": "dădea",
"sbjv|present|3|SG": "dea", "sbjv|present|3|PL": "dea",
"part": "dat", "ger": "dând",
},
"fi": { # a fi — present is in UniMorph but keep participle + subjunctive here
"part": "fost", "ger": "fiind",
"sbjv|present|1|SG": "fiu", "sbjv|present|2|SG": "fii", "sbjv|present|3|SG": "fie",
"sbjv|present|1|PL": "fim", "sbjv|present|2|PL": "fiți", "sbjv|present|3|PL": "fie",
"ind|imperfect|1|SG": "eram", "ind|imperfect|2|SG": "erai",
"ind|imperfect|3|SG": "era", "ind|imperfect|1|PL": "eram",
"ind|imperfect|2|PL": "erați", "ind|imperfect|3|PL": "erau",
},
}
# auxiliary clitic paradigms (person,number)->form
_AUX = {
"perfect": {("first", "singular"): "am", ("second", "singular"): "ai",
("third", "singular"): "a", ("first", "plural"): "am",
("second", "plural"): "ați", ("third", "plural"): "au"},
"future": {("first", "singular"): "voi", ("second", "singular"): "vei",
("third", "singular"): "va", ("first", "plural"): "vom",
("second", "plural"): "veți", ("third", "plural"): "vor"},
"conditional": {("first", "singular"): "", ("second", "singular"): "ai",
("third", "singular"): "ar", ("first", "plural"): "am",
("second", "plural"): "ați", ("third", "plural"): "ar"},
}
def aux(kind, person, number):
return _AUX[kind][(person, number)]
# ── build verb lexicon from UniMorph ──────────────────────────────────────────────
def _build_verbs():
verbs, part, ger = {}, {}, {}
with open(_UNIMORPH, encoding="utf-8") as fh:
for line in fh:
line = line.rstrip("\n")
if not line or "\t" not in line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
lemma, form, tag = parts
f = _feat_set(tag)
head = tag.split(";")[0]
if head == "V.PTCP":
if "PST" in f:
part.setdefault(lemma, form)
continue
if head == "V.CVB":
if "PRS" in f:
ger.setdefault(lemma, form)
continue
if head != "V":
continue
person = next((p for p in ("1", "2", "3") if p in f), None)
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
if person is None or number is None:
continue
# conjunctiv forms in UniMorph carry a leading 'să ' — strip it
surf = form
if surf.startswith(""):
surf = surf[3:]
for (mood, tense), req in _VERB_KEYMAP.items():
if not req <= f:
continue
if tense == "imperfect" and "PFV" in f:
continue
if tense == "perfect_s" and "IPFV" in f:
continue
# keep IND;PRS out of the PRF slot (mai-mult-ca-perfect etc. ignored)
if {"IND", "PRS"} <= req and "PRF" in f:
continue
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), surf)
break
return verbs, part, ger
# ── kaikki nouns: full declension paradigm per lemma ──────────────────────────────
_EXCL = {"alternative", "archaic", "obsolete", "regional", "dialectal", "rare",
"table-tags", "inflection-template", "error-unrecognized-form",
"diminutive", "augmentative", "informal"}
def _noun_key(tagset):
if tagset & _EXCL:
return None
if "vocative" in tagset:
case = "voc"
elif "genitive" in tagset or "dative" in tagset:
case = "gendat"
elif "nominative" in tagset or "accusative" in tagset:
case = "nomacc"
else:
return None
definite = "definite" in tagset and "indefinite" not in tagset
number = "PL" if "plural" in tagset else ("SG" if "singular" in tagset else None)
if number is None:
return None
return (case, definite, number)
def _build_nouns():
nouns = {} # lemma -> {"g":..., para:{(case,def,num):form}, "PL":plain_plural}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
if d.get("pos") != "noun":
continue
word = d.get("word", "")
if not word or " " in word:
continue
ht = d.get("head_templates") or []
g = None
if ht:
a = str((ht[0].get("args") or {}).get("1") or "").lower()
if a[:1] in ("m", "f", "n"):
g = a[:1]
entry = nouns.setdefault(word, {"g": g, "para": {}, "PL": None})
if entry["g"] is None and g:
entry["g"] = g
for x in (d.get("forms") or []):
fm = x.get("form")
tg = set(x.get("tags") or [])
if not fm or fm in ("-", "#", "") or " " in fm:
continue
if tg == {"plural"} and not entry["PL"]:
entry["PL"] = fm
k = _noun_key(tg)
if k and k not in entry["para"]:
entry["para"][k] = fm
return nouns
# ── adjectives from kaikki (UniMorph ron ADJ is sparse AND mis-tagged; kaikki is
# clean: the 4-form agreement pattern bun/bună/buni/bune). Neuter maps sg->masc,
# pl->fem, so 4 forms (m/f × SG/PL) fully cover it. ────────────────────────────
def _build_adjs():
adjs = {} # lemma -> {(gender,number): form} gender in {m,f}
with open(_KAIKKI, encoding="utf-8") as fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
if d.get("pos") != "adj":
continue
word = d.get("word", "")
if not word or " " in word:
continue
d0 = adjs.setdefault(word, {})
d0.setdefault(("m", "SG"), word) # masc sg = headword
for x in (d.get("forms") or []):
fm = x.get("form")
t = set(x.get("tags") or [])
if not fm or " " in fm or fm in ("-", "#") or (t & _EXCL):
continue
if "definite" in t or "genitive" in t or "dative" in t:
continue # keep indefinite nom/acc agr set
pl = "plural" in t
fem = "feminine" in t
masc = "masculine" in t
if fem and pl:
d0.setdefault(("f", "PL"), fm)
elif masc and pl:
d0.setdefault(("m", "PL"), fm)
elif fem and not pl:
d0.setdefault(("f", "SG"), fm)
elif pl and not fem and not masc: # bare plural -> both genders
d0.setdefault(("m", "PL"), fm)
d0.setdefault(("f", "PL"), fm)
return adjs
def _build_cache():
verbs, part, ger = _build_verbs()
nouns = _build_nouns()
adjs = _build_adjs()
data = {"verbs": verbs, "part": part, "ger": ger, "nouns": nouns, "adjs": adjs}
try:
with open(_CACHE, "wb") as fh:
pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL)
except OSError:
pass
return data
def _load():
if os.path.exists(_CACHE):
srcs = [_UNIMORPH, _KAIKKI]
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
if os.path.getmtime(_CACHE) >= newest:
try:
with open(_CACHE, "rb") as fh:
return pickle.load(fh)
except Exception:
pass
return _build_cache()
_LEX = _load()
_VERBS, _PART, _GER, _NOUNS, _ADJS = (
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"])
# ── rule verb conjugation fallback ────────────────────────────────────────────────
def _vclass(lemma):
if lemma.endswith("a"):
return "a"
if lemma.endswith("ea"):
return "ea"
if lemma.endswith("e"):
return "e"
if lemma.endswith("i"):
return "i"
if lemma.endswith("î"):
return "î"
return None
# regular present endings by class [1sg,2sg,3sg,1pl,2pl,3pl]
_REG_PRS = {
"a": ["", "i", "ă", "ăm", "ați", "ă"], # a lucra type (simplified)
"ea": ["", "i", "e", "em", "eți", "", ],
"e": ["", "i", "e", "em", "eți", ""],
"i": ["esc", "ești", "ește", "im", "iți", "esc"], # -i type (a vorbi)
"î": ["ăsc", "ăști", "ăște", "âm", "âți", "ăsc"],
}
_SLOT = {("first", "singular"): 0, ("second", "singular"): 1, ("third", "singular"): 2,
("first", "plural"): 3, ("second", "plural"): 4, ("third", "plural"): 5}
def _rule_conjugate(lemma, mood, tense, person, number):
vc = _vclass(lemma)
if vc is None:
return None
i = _SLOT[(person, number)]
body = lemma[:-len(vc)]
if mood == "ind" and tense == "present":
end = _REG_PRS[vc][i]
return body + end
if mood == "ind" and tense == "imperfect":
# -a/-i/-î -> stem + a/eai...; -e/-ea -> eam. Simplified regular imperfect.
stem = body
endings = {"a": ["am", "ai", "a", "am", "ați", "au"],
"i": ["eam", "eai", "ea", "eam", "eați", "eau"],
"î": ["am", "ai", "a", "am", "ați", "au"],
"e": ["eam", "eai", "ea", "eam", "eați", "eau"],
"ea": ["eam", "eai", "ea", "eam", "eați", "eau"]}[vc]
return stem + endings[i]
return None
# ── PUBLIC verb API ───────────────────────────────────────────────────────────────
def conjugate(lemma, mood, tense, person, number):
lemma = lemma.strip().lower()
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{_NUMBER.get(number,'?')}"
ir = _IRREG.get(lemma)
if ir and key in ir:
return ir[key], "lexicon"
form = _VERBS.get((lemma, key))
if form:
return form, "lexicon"
r = _rule_conjugate(lemma, mood, tense, person, number)
if r is not None:
return r, "rule"
return lemma, "fallback"
def participle(lemma):
"""Past participle — INVARIABLE in the perfect compus (am mers, am văzut)."""
lemma = lemma.strip().lower()
ir = _IRREG.get(lemma)
if ir and "part" in ir:
return ir["part"], "lexicon"
if lemma in _PART:
return _PART[lemma], "lexicon"
vc = _vclass(lemma)
if vc == "a":
return lemma[:-1] + "at", "rule"
if vc in ("ea",):
return lemma[:-2] + "ut", "rule"
if vc == "i":
return lemma[:-1] + "it", "rule"
if vc == "î":
return lemma[:-1] + "ât", "rule"
if vc == "e":
return lemma[:-1] + "ut", "rule"
return lemma, "fallback"
def gerund(lemma):
lemma = lemma.strip().lower()
ir = _IRREG.get(lemma)
if ir and "ger" in ir:
return ir["ger"], "lexicon"
if lemma in _GER:
return _GER[lemma], "lexicon"
vc = _vclass(lemma)
if vc in ("a", "î"):
return lemma[:-1] + "ând", "rule"
if vc in ("ea", "e", "i"):
return lemma[:-len(vc)] + "ind", "rule"
return lemma, "fallback"
# ── noun gender ───────────────────────────────────────────────────────────────────
def noun_gender(lemma):
lemma = lemma.strip().lower()
d = _NOUNS.get(lemma)
if d and d.get("g") in ("m", "f", "n"):
return d["g"]
if lemma.endswith(("ă", "a", "e")):
return "f"
return "m"
# ── SUFFIXED DEFINITE ARTICLE — rule engine (fallback for OOV nouns) ───────────────
def definite_suffix(noun, gender, number, case="nomacc"):
"""Attach the enclitic definite article by gender + ending. Returns (form, conf).
This is the headline Romanian-specific engine extension."""
n = noun
g = gender
if number == "singular":
if g in ("m", "n"):
if case == "gendat":
# masc/neut gen-dat definite: -lui
if n.endswith("e"):
return n + "lui", "rule" # câine -> câinelui
if n.endswith("u"):
return n + "lui", "rule"
return n + "ului", "rule" # om -> omului
# nom/acc
if n.endswith("e"):
return n + "le", "rule" # câine -> câinele
if n.endswith("u"):
return n + "l", "rule" # codru -> codrul
if n.endswith("i"):
return n + "ul", "rule"
return n + "ul", "rule" # om -> omul
# feminine singular
if case == "gendat":
# fem gen/dat definite = plural-stem + i (casei, fetei) — needs plural;
# approximated as: -ă->-ei, -e->-ei, -a->-alei
if n.endswith("ă"):
return n[:-1] + "ei", "rule" # casă -> casei
if n.endswith("e"):
return n[:-1] + "ei", "rule" # carte -> cărții(approx cartei)
if n.endswith("a"):
return n[:-1] + "lei", "rule"
return n + "i", "rule"
# fem nom/acc
if n.endswith("ă"):
return n[:-1] + "a", "rule" # casă -> casa
if n.endswith("e"):
return n[:-1] + "ea", "rule" # carte -> cartea
if n.endswith("a"):
return n + "ua", "rule" # stea -> steaua
if n.endswith("i"):
return n + "a", "rule"
return n + "a", "rule"
# plural
if case == "gendat":
base = noun
return base + "lor", "rule" # -lor for all gen/dat pl
if g == "m":
return noun + "i", "rule" # oameni -> oamenii (+i)
return noun + "le", "rule" # case -> casele, trenuri->trenurile
# ── rule pluralization (fallback) ─────────────────────────────────────────────────
def _rule_plural(noun, gender):
if gender == "f":
if noun.endswith("ă"):
return noun[:-1] + "e"
if noun.endswith("e"):
return noun[:-1] + "i"
if noun.endswith("a"):
return noun[:-1] + "le"
return noun + "e"
if gender == "n":
return noun + "uri"
# masculine
if noun.endswith(("e",)):
return noun[:-1] + "i"
return noun + "i"
# ── PUBLIC noun inflection ────────────────────────────────────────────────────────
def inflect_noun(lemma, number, gender=None, case="nomacc", definite=False):
lemma = lemma.strip().lower()
g = gender or noun_gender(lemma)
d = _NOUNS.get(lemma)
numk = "SG" if number == "singular" else "PL"
if d:
if case == "voc":
form = d["para"].get(("voc", True, numk)) or d["para"].get(("voc", False, numk))
if form:
return form, "lexicon"
# try the exact paradigm cell from kaikki (lexically grounded)
form = d["para"].get((case, definite, numk))
if form:
return form, "lexicon"
# indefinite fallbacks from the paradigm
if not definite:
form = d["para"].get(("nomacc", False, numk))
if form:
return form, "lexicon"
if numk == "PL" and d.get("PL"):
return d["PL"], "lexicon"
if numk == "SG":
return lemma, "lexicon"
# rule path
base = lemma if number == "singular" else _rule_plural(lemma, g)
if definite:
return definite_suffix(base, g, number, case)
return base, ("rule" if d is None else "lexicon")
# ── PUBLIC adjective agreement ────────────────────────────────────────────────────
def _neuter_map(gender, number):
# neuter agrees masculine in SG, feminine in PL
if gender == "n":
return "m" if number == "singular" else "f"
return gender
def inflect_adj(lemma, gender, number, case="nomacc", definite=False):
lemma = lemma.strip().lower()
numk = "SG" if number == "singular" else "PL"
eg = _neuter_map(gender, number) # neuter -> masc(SG)/fem(PL)
d = _ADJS.get(lemma)
if d:
form = d.get((eg, numk))
if form:
return form, "lexicon"
# rule fallback: 4-form pattern bun/bună/buni/bune keyed by effective gender
a = lemma
if number == "singular":
if eg == "f":
if a.endswith("e"):
return a, "rule" # mare invariant sg
if a.endswith("u"):
return a[:-1] + "ă", "rule" # nou -> nouă
if a.endswith("ă"):
return a, "rule"
return a + "ă", "rule" # bun -> bună
return a, "rule" # masc/neut sg = lemma
# plural
if eg == "f":
if a.endswith("e"):
return a[:-1] + "i", "rule" # mare -> mari
if a.endswith("u"):
return a[:-1] + "e", "rule" # nou -> noue (approx; 'noi' irr)
if a.endswith("ă"):
return a[:-1] + "e", "rule"
return a + "e", "rule" # bun -> bune
# masc/neut(SG-only)->here masc pl -> -i
if a.endswith("e"):
return a[:-1] + "i", "rule" # mare -> mari
if a.endswith("u"):
return a[:-1] + "i", "rule"
return a + "i", "rule" # bun -> buni
def lexicon_stats():
return {
"verb_source": "UniMorph Romanian (github.com/unimorph/ron) + curated "
"irregulars (avea/vrea/da + aux clitic paradigms)",
"noun_source": "kaikki.org Romanian — full case/definite/vocative declension",
"adj_source": "UniMorph Romanian ADJ (case×gender×number×definiteness)",
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
"unimorph_verb_forms": len(_VERBS),
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
"irregular_verb_lemmas": len(_IRREG),
"participle_lemmas": len(_PART),
"noun_lemmas": len(_NOUNS),
"adj_lemmas": len(_ADJS),
}
if __name__ == "__main__":
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
print("\n── SUFFIXED DEFINITE ARTICLE (the headline delta) ──")
for n, g in [("om", "m"), ("băiat", "m"), ("casă", "f"), ("carte", "f"),
("tren", "n"), ("student", "m"), ("floare", "f")]:
sg = inflect_noun(n, "singular", g, "nomacc", True)
pl = inflect_noun(n, "plural", g, "nomacc", True)
gd = inflect_noun(n, "singular", g, "gendat", True)
vo = inflect_noun(n, "singular", g, "voc", False)
print(f" {n:8}({g}) def.sg={sg[0]:12} def.pl={pl[0]:14} "
f"gen/dat.sg={gd[0]:12} voc={vo[0]}")
print("\n── NEUTER split agreement (tren: masc SG / fem PL) ──")
print(" tren nou ->", inflect_noun("tren", "singular", "n")[0],
inflect_adj("nou", "n", "singular")[0])
print(" trenuri noi->", inflect_noun("tren", "plural", "n")[0],
inflect_adj("nou", "n", "plural")[0])
print("\n── verbs ──")
for l, m, t, p, n, in [("merge", "ind", "present", "third", "singular"),
("avea", "ind", "present", "first", "singular"),
("fi", "ind", "present", "third", "singular"),
("vorbi", "ind", "present", "third", "plural"),
("face", "sbjv", "present", "third", "singular"),
("lucra", "ind", "imperfect", "third", "singular")]:
print(f" {l:8}{m}/{t:10}{p[:3]}.{n[:2]} -> {conjugate(l,m,t,p,n)}")
print(" perfect-aux(3sg):", aux("perfect", "third", "singular"),
"| future(1sg):", aux("future", "first", "singular"),
"| cond(3sg):", aux("conditional", "third", "singular"))
print(" participle merge/vedea:", participle("merge"), participle("vedea"))
-43
View File
@@ -1,43 +0,0 @@
// multilingual_gate.el - deterministic language detect + localized-phrase test.
fn mg_det(text: String, want: String) -> String {
let got: String = ml_detect(text)
let ok: String = "MISMATCH"
if str_eq(got, want) { let ok = "ok" }
return " detect(" + got + ") want=" + want + " (" + ok + ") :: " + text + "\n"
}
fn mg_ok(text: String, want: String) -> Int {
if str_eq(ml_detect(text), want) { return 1 }
return 0
}
fn run_ml_gate() -> String {
let t1: String = "Does Neuron use SQLite for storage?"
let t2: String = "Neuron, me explica cómo la saliencia forma las geometrías."
let t3: String = "O professor não leu o livro na memória."
let t4: String = "Che cosa memorizza Neuron nella memoria?"
let rep: String = "==== ELP multilingual detect + localized phrases ====\n"
let rep = rep + mg_det(t1, "en")
let rep = rep + mg_det(t2, "es")
let rep = rep + mg_det(t3, "pt")
let rep = rep + mg_det(t4, "it")
let rep = rep + " localized decline (pt): " + ml_tr("no_memory", "pt") + "\n"
let rep = rep + " localized decline (es): " + ml_tr("no_memory", "es") + "\n"
let rep = rep + " term(saliência->en): " + ml_term("saliência", "pt") + "\n"
let rep = rep + " pred(store->pt): " + ml_translate_pred("store", "pt") + "\n"
let ok: Int = 0
if mg_ok(t1, "en") == 1 { let ok = ok + 1 }
if mg_ok(t2, "es") == 1 { let ok = ok + 1 }
if mg_ok(t3, "pt") == 1 { let ok = ok + 1 }
if mg_ok(t4, "it") == 1 { let ok = ok + 1 }
let rep = rep + "-----------------------------------------------------------------\n"
let rep = rep + "language detected correctly: " + int_to_str(ok) + "/4\n"
if ok == 4 { let rep = rep + "ML GATE: PASS\n" } else { let rep = rep + "ML GATE: FAIL\n" }
return rep
}
println(run_ml_gate())
-52
View File
@@ -1,52 +0,0 @@
// propositions_gate.el - the READ primitive over memory text (native el).
// Proves triples are recovered from free memory text and that SACRED polarity
// survives extraction (a negative memory must yield a NOT-triple).
fn pg_check(text: String, want_pol: String) -> String {
let p: [String] = prop_extract_one(text, "nd-test")
let pol: String = slots_get(p, "polarity")
let ok: String = "MISMATCH"
if str_eq(pol, want_pol) { let ok = "ok" }
return " " + prop_repr(p) + " pol=" + pol + " expected=" + want_pol + " (" + ok + ")\n"
}
fn pg_pol_ok(text: String, want_pol: String) -> Int {
let p: [String] = prop_extract_one(text, "nd-test")
if str_eq(slots_get(p, "polarity"), want_pol) { return 1 }
return 0
}
fn run_prop_gate() -> String {
let m1: String = "Neuron stores memories in SQLite."
let m2: String = "The engram does not delete a memory."
let m3: String = "Salience never drops the negation."
let m4: String = "The teacher gives the book to the children."
let rep: String = "==== ELP proposition extraction (memory text -> triples) ====\n"
let rep = rep + pg_check(m1, "aff")
let rep = rep + pg_check(m2, "neg")
let rep = rep + pg_check(m3, "neg")
let rep = rep + pg_check(m4, "aff")
// multi-sentence memory: one triple per sentence, order preserved
let doc: String = "Neuron persists learning. It does not forget the library."
let props: [String] = prop_extract(doc, "nd-doc")
let rep = rep + " --- multi-sentence doc (" + int_to_str(native_list_len(props)) + " props) ---\n"
let di: Int = 0
while di < native_list_len(props) {
let rep = rep + " " + native_list_get(props, di) + "\n"
let di = di + 1
}
let ok: Int = 0
if pg_pol_ok(m1, "aff") == 1 { let ok = ok + 1 }
if pg_pol_ok(m2, "neg") == 1 { let ok = ok + 1 }
if pg_pol_ok(m3, "neg") == 1 { let ok = ok + 1 }
if pg_pol_ok(m4, "aff") == 1 { let ok = ok + 1 }
let rep = rep + "-----------------------------------------------------------------\n"
let rep = rep + "SACRED polarity correct on extraction: " + int_to_str(ok) + "/4\n"
if ok == 4 { let rep = rep + "PROP GATE: PASS\n" } else { let rep = rep + "PROP GATE: FAIL\n" }
return rep
}
println(run_prop_gate())
BIN
View File
Binary file not shown.
+780 -234
View File
File diff suppressed because it is too large Load Diff
+825 -470
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+12
View File
@@ -4342,6 +4342,9 @@ el_val_t builtin_arity(el_val_t name) {
if (str_eq(name, EL_STR("http_serve"))) {
return 2;
}
if (str_eq(name, EL_STR("http_serve_async"))) {
return 2;
}
if (str_eq(name, EL_STR("http_set_handler"))) {
return 1;
}
@@ -4546,6 +4549,12 @@ el_val_t builtin_arity(el_val_t name) {
if (str_eq(name, EL_STR("engram_load"))) {
return 1;
}
if (str_eq(name, EL_STR("engram_load_dir"))) {
return 1;
}
if (str_eq(name, EL_STR("engram_reindex_json"))) {
return 0;
}
if (str_eq(name, EL_STR("engram_get_node_json"))) {
return 1;
}
@@ -4564,6 +4573,9 @@ el_val_t builtin_arity(el_val_t name) {
if (str_eq(name, EL_STR("engram_stats_json"))) {
return 0;
}
if (str_eq(name, EL_STR("engram_apply_decay_json"))) {
return 0;
}
if (str_eq(name, EL_STR("llm_call"))) {
return 2;
}
-711
View File
@@ -1,711 +0,0 @@
/*
* 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);
}
}
}
@@ -1,554 +0,0 @@
# 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
@@ -1,208 +0,0 @@
#!/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
@@ -1,949 +0,0 @@
/*
* 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
@@ -1,844 +0,0 @@
/*
* 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
@@ -1,574 +0,0 @@
/*
* 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
@@ -1,117 +0,0 @@
#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,12 +52,6 @@
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)
@@ -182,7 +176,6 @@ 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
@@ -632,7 +625,6 @@ 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);
@@ -646,12 +638,6 @@ 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
@@ -1,711 +0,0 @@
#!/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
+1
View File
@@ -2502,6 +2502,7 @@ fn builtin_arity(name: String) -> Int {
if str_eq(name, "http_post_with_headers") { return 3 }
if str_eq(name, "http_post_form_auth") { return 3 }
if str_eq(name, "http_serve") { return 2 }
if str_eq(name, "http_serve_async") { return 2 }
if str_eq(name, "http_set_handler") { return 1 }
// Seed primitives (__-prefix) runtime/el_seed.c
if str_eq(name, "__str_len") { return 1 }
+1 -25
View File
@@ -23,29 +23,10 @@ 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)
}
@@ -54,12 +35,7 @@ fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
if k == kind {
return pos + 1
}
// 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
}
// On mismatch just advance; error recovery is best-effort
pos + 1
}
+21 -6
View File
@@ -283,6 +283,12 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
}
exec_command("rm -f " + err_tmp)
// Strip capability-violation guard #error lines injected by elc when a
// module is compiled in isolation (utility context). These are safe to
// remove here: the entire binary is linked under the CGI entry-point
// declaration in soul.el, so the module-level guard is redundant.
exec_command("sed -i.bak '/^#error \"capability violation/d' " + c_out + " && rm -f " + c_out + ".bak")
// Move the generated .elh (written next to the source by elc) into
// out_dir so that #include "module.elh" lines in the generated .c
// files resolve correctly when cc is invoked with -I <out_dir>.
@@ -305,6 +311,10 @@ 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)"
// liboqs (post-quantum crypto) present on macOS dev machines, not on CI
// Linux containers. Link -loqs only when the library is available.
let oqs_lib_flag: String = "$(brew --prefix liboqs 2>/dev/null | xargs -I{} printf -- '-L{}/lib -loqs' 2>/dev/null || true)"
let oqs_inc_flag: String = "$(brew --prefix liboqs 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)"
// 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.
@@ -312,7 +322,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, 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 parts = native_list_append(parts, "cc -O2 -DHAVE_CURL " + bracket_flag + " " + ossl_inc_flag + " " + oqs_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)
@@ -320,7 +330,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
let i = i + 1
}
let parts = native_list_append(parts, runtime_path)
let parts = native_list_append(parts, ossl_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm")
let parts = native_list_append(parts, ossl_lib_flag + " " + oqs_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm")
let parts = native_list_append(parts, "-o " + out_bin)
let cmd: String = str_join(parts, " ")
println(" link " + out_bin)
@@ -432,18 +442,23 @@ fn main() -> Void {
exit(1)
}
// Link use only the entry-point .c file (which elc compiles as a
// monolithic unit, inlining all imports). Linking all module .c files
// together causes duplicate-symbol errors because each module's .c also
// inlines its full import tree.
let entry_c: String = out_dir + "/" + basename_noext(entry) + ".c"
let link_files: [String] = native_list_empty()
let link_files = native_list_append(link_files, entry_c)
// Append any extra C sources declared in the manifest (e.g. platform stubs)
let ei = 0
let en: Int = native_list_len(extra_c)
while ei < en {
let ec: String = native_list_get(extra_c, ei)
let c_files = native_list_append(c_files, ec)
let link_files = native_list_append(link_files, ec)
let ei = ei + 1
}
// Link
let out_bin: String = out_dir + "/" + pkg_name
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
let linked: Bool = link_binary(link_files, out_bin, runtime_path, out_dir, dry_run)
if !linked {
println("elb: link failed")
exit(1)
+5
View File
@@ -0,0 +1,5 @@
CompileFlags:
Add:
- -I/opt/homebrew/Cellar/liboqs/0.15.0/include
- -I/opt/homebrew/opt/openssl@3/include
- -std=c11
File diff suppressed because it is too large Load Diff
+22 -43
View File
@@ -117,15 +117,6 @@ 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, ...);
@@ -151,8 +142,8 @@ 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_serve_async(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
/* HTTP server v2 ─────────────────────────────────────────────────────────────
@@ -177,11 +168,6 @@ 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,...}`
@@ -444,10 +430,22 @@ el_val_t str_to_float(el_val_t s);
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_exp(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);
/* ── Float arithmetic builtins (correct IEEE 754 via bit-cast round-trip) ─── */
el_val_t float_add(el_val_t a, el_val_t b);
el_val_t float_sub(el_val_t a, el_val_t b);
el_val_t float_mul(el_val_t a, el_val_t b);
el_val_t float_div(el_val_t a, el_val_t b);
el_val_t float_gt(el_val_t a, el_val_t b);
el_val_t float_lt(el_val_t a, el_val_t b);
el_val_t float_eq(el_val_t a, el_val_t b);
el_val_t float_gte(el_val_t a, el_val_t b);
el_val_t float_lte(el_val_t a, el_val_t b);
/* ── String additions ────────────────────────────────────────────────────── */
el_val_t str_index_of(el_val_t s, el_val_t sub);
@@ -507,6 +505,7 @@ el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_joi
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_set(el_val_t list, el_val_t index, el_val_t value);
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);
@@ -591,7 +590,6 @@ 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);
@@ -605,45 +603,26 @@ el_val_t engram_edge_count(void);
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);
el_val_t engram_load_dir(el_val_t data_dir);
el_val_t engram_reindex_json(void);
el_val_t engram_write_binary_el(el_val_t path);
el_val_t engram_load_binary_el(el_val_t path);
/* 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_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);
el_val_t engram_wm_avg_weight(void); /* avg wm weight of promoted nodes; float bits */
el_val_t engram_wm_top_json(el_val_t n); /* top-N WM nodes by weight as compact JSON */
el_val_t engram_apply_decay_json(void);
el_val_t engram_list_layers_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
+3 -43
View File
@@ -6,55 +6,15 @@
//
// 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)
}
// 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)
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)
}
// --- Node retrieval ---
@@ -1,6 +0,0 @@
local.properties
.gradle/
build/
app/build/
*.iml
.idea/
@@ -1,55 +0,0 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.neuron.el'
compileSdk 34
defaultConfig {
applicationId "com.neuron.el"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_STL=c++_shared"
}
}
ndk {
// Build for the two most relevant ABIs. Add x86/x86_64 for emulator.
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
}
}
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
version "3.22.1"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
jniDebuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
// No third-party dependencies el-native uses only android.* framework classes.
implementation 'androidx.appcompat:appcompat:1.6.1'
}
@@ -1,4 +0,0 @@
# Add project specific ProGuard rules here.
# Keep ElBridge and MainActivity so JNI symbol names stay intact.
-keep class com.neuron.el.ElBridge { *; }
-keep class com.neuron.el.MainActivity { *; }
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".ElApp"
android:label="el-native"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:allowBackup="false"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -1,18 +0,0 @@
package com.neuron.el;
import android.app.Application;
/**
* ElApp Application subclass for native-hello-android.
*
* Currently minimal: exists as an anchor for future app-level initialisation
* (crash reporting, global state, etc.). Listed in AndroidManifest.xml as
* android:name=".ElApp".
*/
public class ElApp extends Application {
@Override
public void onCreate() {
super.onCreate();
}
}
@@ -1,711 +0,0 @@
/*
* 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);
}
}
}
@@ -1,38 +0,0 @@
package com.neuron.el;
import android.app.Activity;
import android.os.Bundle;
/**
* MainActivity entry point for native-hello-android.
*
* Loads the el native shared library, initialises ElBridge with the Activity
* reference (required before any widget operations), then hands control to the
* compiled el program via nativeMain().
*
* The el boot sequence (native_init window_from_manifest app_build
* window_show) runs inside nativeMain. __native_run_loop is a no-op on Android;
* the Activity lifecycle owns the UI thread after onCreate returns.
*/
public class MainActivity extends Activity {
static {
System.loadLibrary("elnative");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Register this Activity with the bridge BEFORE nativeMain so that
// el_android_init can look up ElBridge methods and __native_init works.
ElBridge.init(this);
// Run the compiled el program.
nativeMain();
}
/**
* Implemented in el_android.c as Java_com_neuron_el_MainActivity_nativeMain.
* Calls the el program's main(), which runs the full boot sequence.
*/
private native void nativeMain();
}
@@ -1,57 +0,0 @@
cmake_minimum_required(VERSION 3.22.1)
project("elnative")
#
# CMakeLists.txt for native-hello-android
#
# Sources:
# el_android.c Android JNI widget bridge
# el_seed.c OS-boundary __-prefixed primitives
# el_runtime.c High-level el builtins (str_len, json_*, etc.)
# el_native_vessel.c el-native vessel (compiled from vessels/el-native/src/main.el)
# native_hello.c App entry point (compiled from examples/native-hello/src/main.el)
#
# el_runtime.c and el_seed.c both define __-prefixed symbols. On Android the
# linker accepts duplicate weak symbols; the shared library loads one copy.
# The EL_TARGET_ANDROID guard in el_android.c ensures only the Android widget
# backend is compiled in.
#
add_library(
elnative
SHARED
# native_hello.c listed first so its main() takes precedence over the
# vessel's main() when --allow-multiple-definition is in effect.
native_hello.c
el_native_vessel.c
el_android.c
el_seed.c
el_runtime.c
)
# EL_TARGET_ANDROID activates the Android JNI widget backend in el_android.c
# and the corresponding guards in el_seed.c / el_native_target.h.
target_compile_definitions(elnative PRIVATE
EL_TARGET_ANDROID
)
target_include_directories(elnative PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
# el_runtime.c and el_seed.c both define __-prefixed OS-boundary symbols.
# el_runtime.c's copies are weaker definitions; allow-multiple-definition lets
# the linker pick one copy silently (el_seed.c listed later = its copy wins
# in the Android linker's right-to-left resolution order).
target_link_options(elnative PRIVATE
"-Wl,--allow-multiple-definition"
)
find_library(log-lib log)
find_library(android-lib android)
target_link_libraries(elnative
${log-lib}
${android-lib}
)
@@ -1,964 +0,0 @@
/*
* 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);
}
/* ── MainActivity JNI entry point ─────────────────────────────────────────── */
/*
* Java_com_neuron_el_MainActivity_nativeMain invoked from MainActivity.onCreate
* after ElBridge.init(this). Calls the el program's compiled main() which runs
* the boot sequence: native_init window_from_manifest app_build window_show.
* __native_run_loop is a no-op on Android; the Activity lifecycle drives the UI.
*/
JNIEXPORT void JNICALL
Java_com_neuron_el_MainActivity_nativeMain(JNIEnv *env, jobject obj) {
(void)env; (void)obj;
extern int main(int argc, char **argv);
char *argv[] = {"el-app", NULL};
main(1, argv);
}
#endif /* EL_TARGET_ANDROID */
@@ -1,573 +0,0 @@
/*
* 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);
/* 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 */
@@ -1,459 +0,0 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t native_init(void);
el_val_t native_run_loop(void);
el_val_t manifest_read(el_val_t path);
el_val_t manifest_title(el_val_t m);
el_val_t manifest_width(el_val_t m);
el_val_t manifest_height(el_val_t m);
el_val_t manifest_min_width(el_val_t m);
el_val_t manifest_min_height(el_val_t m);
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);
el_val_t window_from_manifest(el_val_t manifest_path);
el_val_t window_show(el_val_t handle);
el_val_t window_set_title(el_val_t handle, el_val_t title);
el_val_t vstack(el_val_t spacing);
el_val_t vstack_tight(void);
el_val_t hstack(el_val_t spacing);
el_val_t zstack(void);
el_val_t scroll(void);
el_val_t label(el_val_t text);
el_val_t button(el_val_t label);
el_val_t text_field(el_val_t placeholder);
el_val_t text_area(el_val_t placeholder);
el_val_t image(el_val_t path_or_name);
el_val_t widget_set_text(el_val_t handle, el_val_t text);
el_val_t widget_get_text(el_val_t handle);
el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a);
el_val_t 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_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold);
el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left);
el_val_t widget_set_padding_all(el_val_t handle, el_val_t p);
el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py);
el_val_t widget_set_width(el_val_t handle, el_val_t width);
el_val_t widget_set_height(el_val_t handle, el_val_t height);
el_val_t widget_set_flex(el_val_t handle, el_val_t flex);
el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius);
el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled);
el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden);
el_val_t widget_add_child(el_val_t parent, el_val_t child);
el_val_t widget_remove_child(el_val_t parent, el_val_t child);
el_val_t widget_destroy(el_val_t handle);
el_val_t widget_on_click(el_val_t handle, el_val_t fn_name);
el_val_t widget_on_change(el_val_t handle, el_val_t fn_name);
el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name);
el_val_t hex_channel(el_val_t s, el_val_t offset);
el_val_t hex_nibble(el_val_t c);
el_val_t color_hex_r(el_val_t hex);
el_val_t color_hex_g(el_val_t hex);
el_val_t color_hex_b(el_val_t hex);
el_val_t color_hex_a(el_val_t hex);
el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex);
el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex);
el_val_t style_surface(el_val_t handle);
el_val_t style_button_primary(el_val_t handle);
el_val_t style_label_body(el_val_t handle);
el_val_t style_label_heading(el_val_t handle);
el_val_t style_label_muted(el_val_t handle);
el_val_t TOKEN_PRIMARY;
el_val_t TOKEN_ON_PRIMARY;
el_val_t TOKEN_BACKGROUND;
el_val_t TOKEN_ON_BG;
el_val_t TOKEN_SURFACE;
el_val_t TOKEN_ON_SURFACE;
el_val_t TOKEN_OUTLINE;
el_val_t TOKEN_ERROR;
el_val_t native_init(void) {
__native_init();
return 0;
}
el_val_t native_run_loop(void) {
__native_run_loop();
return 0;
}
el_val_t manifest_read(el_val_t path) {
return __manifest_read(path);
return 0;
}
el_val_t manifest_title(el_val_t m) {
return json_get_string(m, EL_STR("title"));
return 0;
}
el_val_t manifest_width(el_val_t m) {
return json_get_int(m, EL_STR("width"));
return 0;
}
el_val_t manifest_height(el_val_t m) {
return json_get_int(m, EL_STR("height"));
return 0;
}
el_val_t manifest_min_width(el_val_t m) {
return json_get_int(m, EL_STR("min_width"));
return 0;
}
el_val_t manifest_min_height(el_val_t m) {
return json_get_int(m, EL_STR("min_height"));
return 0;
}
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 __window_create(title, width, height, min_width, min_height);
return 0;
}
el_val_t window_from_manifest(el_val_t manifest_path) {
el_val_t m = manifest_read(manifest_path);
el_val_t title = manifest_title(m);
el_val_t w = manifest_width(m);
el_val_t h = manifest_height(m);
el_val_t mw = manifest_min_width(m);
el_val_t mh = manifest_min_height(m);
el_val_t safe_w = ({ el_val_t _if_result_1 = 0; if ((w > 0)) { _if_result_1 = (w); } else { _if_result_1 = (1200); } _if_result_1; });
el_val_t safe_h = ({ el_val_t _if_result_2 = 0; if ((h > 0)) { _if_result_2 = (h); } else { _if_result_2 = (800); } _if_result_2; });
el_val_t safe_mw = ({ el_val_t _if_result_3 = 0; if ((mw > 0)) { _if_result_3 = (mw); } else { _if_result_3 = (600); } _if_result_3; });
el_val_t safe_mh = ({ el_val_t _if_result_4 = 0; if ((mh > 0)) { _if_result_4 = (mh); } else { _if_result_4 = (400); } _if_result_4; });
el_val_t safe_t = ({ el_val_t _if_result_5 = 0; if ((str_len(title) > 0)) { _if_result_5 = (title); } else { _if_result_5 = (EL_STR("App")); } _if_result_5; });
return window_create(safe_t, safe_w, safe_h, safe_mw, safe_mh);
return 0;
}
el_val_t window_show(el_val_t handle) {
__window_show(handle);
return 0;
}
el_val_t window_set_title(el_val_t handle, el_val_t title) {
__window_set_title(handle, title);
return 0;
}
el_val_t vstack(el_val_t spacing) {
return __vstack_create(spacing);
return 0;
}
el_val_t vstack_tight(void) {
return __vstack_create(0);
return 0;
}
el_val_t hstack(el_val_t spacing) {
return __hstack_create(spacing);
return 0;
}
el_val_t zstack(void) {
return __zstack_create();
return 0;
}
el_val_t scroll(void) {
return __scroll_create();
return 0;
}
el_val_t label(el_val_t text) {
return __label_create(text);
return 0;
}
el_val_t button(el_val_t label) {
return __button_create(label);
return 0;
}
el_val_t text_field(el_val_t placeholder) {
return __text_field_create(placeholder);
return 0;
}
el_val_t text_area(el_val_t placeholder) {
return __text_area_create(placeholder);
return 0;
}
el_val_t image(el_val_t path_or_name) {
return __image_create(path_or_name);
return 0;
}
el_val_t widget_set_text(el_val_t handle, el_val_t text) {
__widget_set_text(handle, text);
return 0;
}
el_val_t widget_get_text(el_val_t handle) {
return __widget_get_text(handle);
return 0;
}
el_val_t widget_set_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) {
__widget_set_color(handle, r, g, b, a);
return 0;
}
el_val_t widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g, el_val_t b, el_val_t a) {
__widget_set_bg_color(handle, r, g, b, a);
return 0;
}
el_val_t widget_set_font(el_val_t handle, el_val_t family, el_val_t size, el_val_t bold) {
el_val_t bold_int = ({ el_val_t _if_result_6 = 0; if (bold) { _if_result_6 = (1); } else { _if_result_6 = (0); } _if_result_6; });
__widget_set_font(handle, family, size, bold_int);
return 0;
}
el_val_t widget_set_padding(el_val_t handle, el_val_t top, el_val_t right, el_val_t bottom, el_val_t left) {
__widget_set_padding(handle, top, right, bottom, left);
return 0;
}
el_val_t widget_set_padding_all(el_val_t handle, el_val_t p) {
__widget_set_padding(handle, p, p, p, p);
return 0;
}
el_val_t widget_set_padding_xy(el_val_t handle, el_val_t px, el_val_t py) {
__widget_set_padding(handle, py, px, py, px);
return 0;
}
el_val_t widget_set_width(el_val_t handle, el_val_t width) {
__widget_set_width(handle, width);
return 0;
}
el_val_t widget_set_height(el_val_t handle, el_val_t height) {
__widget_set_height(handle, height);
return 0;
}
el_val_t widget_set_flex(el_val_t handle, el_val_t flex) {
__widget_set_flex(handle, flex);
return 0;
}
el_val_t widget_set_corner_radius(el_val_t handle, el_val_t radius) {
__widget_set_corner_radius(handle, radius);
return 0;
}
el_val_t widget_set_disabled(el_val_t handle, el_val_t disabled) {
el_val_t d = ({ el_val_t _if_result_7 = 0; if (disabled) { _if_result_7 = (1); } else { _if_result_7 = (0); } _if_result_7; });
__widget_set_disabled(handle, d);
return 0;
}
el_val_t widget_set_hidden(el_val_t handle, el_val_t hidden) {
el_val_t h = ({ el_val_t _if_result_8 = 0; if (hidden) { _if_result_8 = (1); } else { _if_result_8 = (0); } _if_result_8; });
__widget_set_hidden(handle, h);
return 0;
}
el_val_t widget_add_child(el_val_t parent, el_val_t child) {
__widget_add_child(parent, child);
return 0;
}
el_val_t widget_remove_child(el_val_t parent, el_val_t child) {
__widget_remove_child(parent, child);
return 0;
}
el_val_t widget_destroy(el_val_t handle) {
__widget_destroy(handle);
return 0;
}
el_val_t widget_on_click(el_val_t handle, el_val_t fn_name) {
__widget_on_click(handle, fn_name);
return 0;
}
el_val_t widget_on_change(el_val_t handle, el_val_t fn_name) {
__widget_on_change(handle, fn_name);
return 0;
}
el_val_t widget_on_submit(el_val_t handle, el_val_t fn_name) {
__widget_on_submit(handle, fn_name);
return 0;
}
el_val_t hex_channel(el_val_t s, el_val_t offset) {
el_val_t hi = str_slice(s, offset, (offset + 1));
el_val_t lo = str_slice(s, (offset + 1), (offset + 2));
el_val_t h = hex_nibble(hi);
el_val_t l = hex_nibble(lo);
return ((h * 16) + l);
return 0;
}
el_val_t hex_nibble(el_val_t c) {
if (str_eq(c, EL_STR("0"))) {
return 0;
}
if (str_eq(c, EL_STR("1"))) {
return 1;
}
if (str_eq(c, EL_STR("2"))) {
return 2;
}
if (str_eq(c, EL_STR("3"))) {
return 3;
}
if (str_eq(c, EL_STR("4"))) {
return 4;
}
if (str_eq(c, EL_STR("5"))) {
return 5;
}
if (str_eq(c, EL_STR("6"))) {
return 6;
}
if (str_eq(c, EL_STR("7"))) {
return 7;
}
if (str_eq(c, EL_STR("8"))) {
return 8;
}
if (str_eq(c, EL_STR("9"))) {
return 9;
}
if (str_eq(c, EL_STR("a"))) {
return 10;
}
if (str_eq(c, EL_STR("b"))) {
return 11;
}
if (str_eq(c, EL_STR("c"))) {
return 12;
}
if (str_eq(c, EL_STR("d"))) {
return 13;
}
if (str_eq(c, EL_STR("e"))) {
return 14;
}
if (str_eq(c, EL_STR("f"))) {
return 15;
}
if (str_eq(c, EL_STR("A"))) {
return 10;
}
if (str_eq(c, EL_STR("B"))) {
return 11;
}
if (str_eq(c, EL_STR("C"))) {
return 12;
}
if (str_eq(c, EL_STR("D"))) {
return 13;
}
if (str_eq(c, EL_STR("E"))) {
return 14;
}
if (str_eq(c, EL_STR("F"))) {
return 15;
}
return 0;
return 0;
}
el_val_t color_hex_r(el_val_t hex) {
el_val_t s = ({ el_val_t _if_result_9 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_9 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_9 = (hex); } _if_result_9; });
el_val_t v = hex_channel(s, 0);
return float_div(int_to_float(v), el_from_float(255.0));
return 0;
}
el_val_t color_hex_g(el_val_t hex) {
el_val_t s = ({ el_val_t _if_result_10 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_10 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_10 = (hex); } _if_result_10; });
el_val_t v = hex_channel(s, 2);
return float_div(int_to_float(v), el_from_float(255.0));
return 0;
}
el_val_t color_hex_b(el_val_t hex) {
el_val_t s = ({ el_val_t _if_result_11 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_11 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_11 = (hex); } _if_result_11; });
el_val_t v = hex_channel(s, 4);
return float_div(int_to_float(v), el_from_float(255.0));
return 0;
}
el_val_t color_hex_a(el_val_t hex) {
el_val_t s = ({ el_val_t _if_result_12 = 0; if (str_starts_with(hex, EL_STR("#"))) { _if_result_12 = (str_slice(hex, 1, str_len(hex))); } else { _if_result_12 = (hex); } _if_result_12; });
if (str_len(s) < 8) {
return el_from_float(1.0);
}
el_val_t v = hex_channel(s, 6);
return float_div(int_to_float(v), el_from_float(255.0));
return 0;
}
el_val_t widget_set_color_hex(el_val_t handle, el_val_t hex) {
widget_set_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex));
return 0;
}
el_val_t widget_set_bg_color_hex(el_val_t handle, el_val_t hex) {
widget_set_bg_color(handle, color_hex_r(hex), color_hex_g(hex), color_hex_b(hex), color_hex_a(hex));
return 0;
}
el_val_t style_surface(el_val_t handle) {
widget_set_bg_color_hex(handle, TOKEN_SURFACE);
widget_set_corner_radius(handle, 8);
return 0;
}
el_val_t style_button_primary(el_val_t handle) {
widget_set_bg_color_hex(handle, TOKEN_PRIMARY);
widget_set_color_hex(handle, TOKEN_ON_PRIMARY);
widget_set_corner_radius(handle, 8);
widget_set_padding_xy(handle, 16, 8);
return 0;
}
el_val_t style_label_body(el_val_t handle) {
widget_set_color_hex(handle, TOKEN_ON_BG);
widget_set_font(handle, EL_STR("system"), 14, 0);
return 0;
}
el_val_t style_label_heading(el_val_t handle) {
widget_set_color_hex(handle, TOKEN_ON_BG);
widget_set_font(handle, EL_STR("system"), 20, 1);
return 0;
}
el_val_t style_label_muted(el_val_t handle) {
widget_set_color_hex(handle, TOKEN_OUTLINE);
widget_set_font(handle, EL_STR("system"), 12, 0);
return 0;
}
int el_vessel_main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
TOKEN_PRIMARY = EL_STR("#60a5fa");
TOKEN_ON_PRIMARY = EL_STR("#0f172a");
TOKEN_BACKGROUND = EL_STR("#0f172a");
TOKEN_ON_BG = EL_STR("#f8fafc");
TOKEN_SURFACE = EL_STR("#1e293b");
TOKEN_ON_SURFACE = EL_STR("#f8fafc");
TOKEN_OUTLINE = EL_STR("#475569");
TOKEN_ERROR = EL_STR("#f87171");
return 0;
}
File diff suppressed because it is too large Load Diff
@@ -1,883 +0,0 @@
/*
* 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
* EL_NULL null / zero value
* EL_FALSE boolean false (0)
* EL_TRUE boolean true (1)
*
* 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 el-compiler/runtime -lcurl -lpthread \
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
*
* With liboqs (post-quantum stack):
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
* -o <out> <prog>.c el-compiler/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)
#define EL_FALSE ((el_val_t)0)
#define EL_TRUE ((el_val_t)1)
/* 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 ──────────────────────────────────────────────────────────────────── */
el_val_t println(el_val_t s);
el_val_t 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 native_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);
/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */
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_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
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_serve(el_val_t port, el_val_t handler);
el_val_t 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. */
el_val_t http_serve_v2(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
* 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);
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
* The getter is exposed as __http_conn_fd() to El programs. */
void el_seed_set_http_conn_fd(int fd);
/* 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);
el_val_t html_raw(el_val_t s);
el_val_t html_escape(el_val_t s);
/* ── 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_list_json(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);
el_val_t json_escape_string(el_val_t sv);
el_val_t json_build_object(el_val_t kvs);
el_val_t json_build_array(el_val_t items);
/* ── 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);
el_val_t now_ns(void);
/* ── 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);
el_val_t state_has(el_val_t key);
el_val_t state_get_or(el_val_t key, el_val_t default_val);
/* ── 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 ─────────────────────────────────────────────────────────────── */
el_val_t exit_program(el_val_t code);
el_val_t getpid_now(void);
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
* exits with code 1 if resident memory exceeds the limit. Call periodically
* during long compilation loops (e.g. after each function is compiled).
* Returns 0 when memory is within bounds. */
el_val_t el_mem_check(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_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);
/* 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_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_list_layers_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);
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) */
/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */
el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */
el_val_t stdout_restore(void); /* restore process stdout to terminal */
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);
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
el_val_t __thread_join(el_val_t tid_v);
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
* The El self-hosting compiler emits calls to __-prefixed names. These are
* forwarding wrappers around the existing el_runtime functions above. */
/* I/O */
el_val_t __println(el_val_t s);
el_val_t __print(el_val_t s);
el_val_t __readline(void);
/* String */
el_val_t __int_to_str(el_val_t n);
el_val_t __str_to_int(el_val_t s);
el_val_t __float_to_str(el_val_t f);
el_val_t __str_to_float(el_val_t s);
el_val_t __str_len(el_val_t s);
el_val_t __str_char_at(el_val_t s, el_val_t i);
el_val_t __str_cmp(el_val_t a, el_val_t b);
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
el_val_t __str_alloc(el_val_t n);
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
/* URL encoding */
el_val_t __url_encode(el_val_t s);
el_val_t __url_decode(el_val_t s);
/* Environment */
el_val_t __env_get(el_val_t key);
/* Subprocess */
el_val_t __exec(el_val_t cmd);
el_val_t __exec_bg(el_val_t cmd);
/* Process */
el_val_t __exit_program(el_val_t code);
/* Filesystem */
el_val_t __fs_exists(el_val_t path);
el_val_t __fs_mkdir(el_val_t path);
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_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
el_val_t __fs_list_raw(el_val_t path);
/* HTTP server */
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
el_val_t __http_serve(el_val_t port, el_val_t handler);
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
el_val_t __http_conn_fd(void);
el_val_t __http_sse_open(el_val_t conn_id);
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
el_val_t __http_sse_close(el_val_t conn_id);
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_map, el_val_t timeout_ms);
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t timeout_ms);
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t output_path);
/* JSON */
el_val_t __json_array_get(el_val_t json, el_val_t index);
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
el_val_t __json_array_len(el_val_t json);
el_val_t __json_get(el_val_t json, el_val_t key);
el_val_t __json_get_raw(el_val_t json, el_val_t key);
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
el_val_t __json_parse_map(el_val_t json_str);
el_val_t __json_stringify_val(el_val_t val);
/* Hashing */
el_val_t __sha256_hex(el_val_t s);
/* State K/V */
el_val_t __state_del(el_val_t key);
el_val_t __state_get(el_val_t key);
el_val_t __state_keys(void);
el_val_t __state_set(el_val_t key, el_val_t val);
/* UUID */
el_val_t __uuid_v4(void);
/* Args */
el_val_t __args_json(void);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
@@ -1,123 +0,0 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t on_greet_click(el_val_t widget, el_val_t data);
el_val_t on_counter_click(el_val_t widget, el_val_t data);
el_val_t on_name_change(el_val_t widget, el_val_t data);
el_val_t app_build(el_val_t window);
el_val_t g_window;
el_val_t g_label;
el_val_t g_input;
el_val_t g_button;
el_val_t g_counter;
el_val_t g_counter_lbl;
el_val_t manifest_env;
el_val_t manifest_path;
el_val_t win;
el_val_t g_window;
el_val_t on_greet_click(el_val_t widget, el_val_t data) {
el_val_t name = widget_get_text(g_input);
el_val_t greeting = ({ el_val_t _if_result_1 = 0; if ((str_len(name) > 0)) { _if_result_1 = (el_str_concat(el_str_concat(EL_STR("Hello, "), name), EL_STR("!"))); } else { _if_result_1 = (EL_STR("Hello, World!")); } _if_result_1; });
widget_set_text(g_label, greeting);
return 0;
}
el_val_t on_counter_click(el_val_t widget, el_val_t data) {
el_val_t g_counter = (g_counter + 1);
widget_set_text(g_counter_lbl, el_str_concat(EL_STR("Clicks: "), int_to_str(g_counter)));
return 0;
}
el_val_t on_name_change(el_val_t widget, el_val_t data) {
el_val_t has_text = (str_len(data) > 0);
widget_set_disabled(g_button, !has_text);
return 0;
}
el_val_t app_build(el_val_t window) {
el_val_t root = vstack(0);
widget_set_padding_all(root, 24);
widget_set_bg_color_hex(root, EL_STR("#0f172a"));
widget_set_flex(root, 1);
el_val_t title = label(EL_STR("el-native \xe2\x80\x94 native widget demo"));
style_label_heading(title);
widget_add_child(root, title);
el_val_t gap1 = label(EL_STR(""));
widget_set_height(gap1, 16);
widget_add_child(root, gap1);
el_val_t subtitle = label(EL_STR("AppKit controls from el code, no ObjC in the app layer."));
style_label_muted(subtitle);
widget_add_child(root, subtitle);
el_val_t gap2 = label(EL_STR(""));
widget_set_height(gap2, 24);
widget_add_child(root, gap2);
el_val_t input_row = hstack(8);
el_val_t name_label = label(EL_STR("Name:"));
style_label_body(name_label);
widget_set_width(name_label, 60);
el_val_t input = text_field(EL_STR("Enter your name\xe2\x80\xa6"));
style_label_body(input);
widget_set_flex(input, 1);
widget_on_change(input, EL_STR("on_name_change"));
el_val_t g_input = input;
el_val_t greet_btn = button(EL_STR("Greet"));
style_button_primary(greet_btn);
widget_set_disabled(greet_btn, 1);
widget_on_click(greet_btn, EL_STR("on_greet_click"));
el_val_t g_button = greet_btn;
widget_add_child(input_row, name_label);
widget_add_child(input_row, input);
widget_add_child(input_row, greet_btn);
widget_add_child(root, input_row);
el_val_t gap3 = label(EL_STR(""));
widget_set_height(gap3, 12);
widget_add_child(root, gap3);
el_val_t greeting = label(EL_STR("Waiting for name\xe2\x80\xa6"));
style_label_body(greeting);
widget_set_color_hex(greeting, EL_STR("#60a5fa"));
widget_set_font(greeting, EL_STR("system"), 16, 1);
el_val_t g_label = greeting;
widget_add_child(root, greeting);
el_val_t gap4 = label(EL_STR(""));
widget_set_height(gap4, 24);
widget_add_child(root, gap4);
el_val_t counter_row = hstack(12);
el_val_t counter_lbl = label(EL_STR("Clicks: 0"));
style_label_body(counter_lbl);
el_val_t g_counter_lbl = counter_lbl;
el_val_t counter_btn = button(EL_STR("Click me"));
style_button_primary(counter_btn);
widget_on_click(counter_btn, EL_STR("on_counter_click"));
widget_add_child(counter_row, counter_lbl);
widget_add_child(counter_row, counter_btn);
widget_add_child(root, counter_row);
widget_add_child(window, root);
return 0;
}
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
g_window = (-1);
g_label = (-1);
g_input = (-1);
g_button = (-1);
g_counter = 0;
g_counter_lbl = (-1);
native_init();
manifest_env = env(EL_STR("EL_MANIFEST"));
manifest_path = ({ el_val_t _if_result_2 = 0; if ((str_len(manifest_env) > 0)) { _if_result_2 = (manifest_env); } else { _if_result_2 = (EL_STR("manifest.el")); } _if_result_2; });
win = window_from_manifest(manifest_path);
g_window = win;
if (win < 0) {
println(EL_STR("Error: failed to create window. Is EL_TARGET_MACOS defined?"));
exit_program(1);
}
app_build(win);
window_show(win);
native_run_loop();
return 0;
}
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
activity_main.xml — placeholder layout.
The el program sets the content view programmatically via ElBridge.setContentView
(called from __window_show → el_android_window_show). This layout is not used
at runtime but satisfies Gradle's res/ validation checks.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">el-native</string>
</resources>
@@ -1,4 +0,0 @@
// Top-level build file configuration for all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.2' apply false
}
-83
View File
@@ -1,83 +0,0 @@
#!/usr/bin/env bash
# build.sh — Build native-hello-android.
#
# Usage:
# ./build.sh # regenerate C sources + ./gradlew assembleDebug
# ./build.sh gen # regenerate C sources only
# ./build.sh gradle # run ./gradlew assembleDebug (skip elc step)
# ./build.sh clean # clean Gradle build outputs
#
# Requirements:
# - Android SDK with NDK and CMake installed
# - local.properties present (copy from local.properties.template and edit sdk.dir)
# - Java 11+ on PATH (for Gradle)
# - elc at ../../../lang/dist/platform/elc
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang"
EL_UI_ROOT="${SCRIPT_DIR}/../.."
EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime"
EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el"
EL_APP_ENTRY="${EL_UI_ROOT}/examples/native-hello/src/main.el"
EL_MANIFEST="${EL_UI_ROOT}/examples/native-hello/manifest.el"
JNI_DIR="${SCRIPT_DIR}/app/src/main/jni"
# Locate elc.
if command -v elc &>/dev/null; then
ELC="elc"
elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then
ELC="${EL_LANG_ROOT}/dist/platform/elc"
else
echo "Error: elc not found. Add it to PATH or place it at:"
echo " ${EL_LANG_ROOT}/dist/platform/elc"
exit 1
fi
gen() {
echo "==> Generating native_hello.c..."
EL_MANIFEST="${EL_MANIFEST}" \
"${ELC}" "${EL_APP_ENTRY}" \
> "${JNI_DIR}/native_hello.c"
echo "==> Generating el_native_vessel.c..."
"${ELC}" "${EL_NATIVE_VESSEL}" \
> "${JNI_DIR}/el_native_vessel.c"
# Rename the vessel's main() to avoid symbol collision with native_hello.c's main().
# The Android JNI entry point (nativeMain) calls native_hello.c's main() explicitly.
sed -i '' 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \
"${JNI_DIR}/el_native_vessel.c" 2>/dev/null || \
sed -i 's/^int main(int _argc, char\*\* _argv) {/int el_vessel_main(int _argc, char** _argv) {/' \
"${JNI_DIR}/el_native_vessel.c"
echo "==> C source generation complete."
}
gradle_build() {
echo "==> Running ./gradlew assembleDebug..."
cd "${SCRIPT_DIR}"
./gradlew assembleDebug
APK_PATH="${SCRIPT_DIR}/app/build/outputs/apk/debug/app-debug.apk"
if [ -f "${APK_PATH}" ]; then
echo "==> Build complete."
echo " APK: ${APK_PATH}"
else
echo "==> Build complete (APK path may differ — check app/build/outputs/apk/)."
fi
}
clean() {
echo "==> Cleaning..."
cd "${SCRIPT_DIR}"
./gradlew clean
echo "==> Clean complete."
}
case "${1:-all}" in
gen) gen ;;
gradle) gradle_build ;;
clean) clean ;;
all|*) gen && gradle_build ;;
esac
@@ -1,3 +0,0 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=false
@@ -1,6 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-122
View File
@@ -1,122 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Gradle start up script for POSIX generated by "gradle init".
#
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
# Make relative path absolute, based on the path of the symlink:
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
;;
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$@"
exec "$JAVACMD" "$@"

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